Skip to content

Instantly share code, notes, and snippets.

@am-kantox
Created February 4, 2023 09:42
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save am-kantox/03325197ff9608dfa83457dbe0d144e9 to your computer and use it in GitHub Desktop.
Save am-kantox/03325197ff9608dfa83457dbe0d144e9 to your computer and use it in GitHub Desktop.
`MapSet` matchers/guards
defmodule MapSetEx do
@moduledoc """
The helper functions to work with mapsets.
Mapsets in matches are matched the same way as maps, that said the following would be matched
```elixir
fn mapset() -> :ok end.(MapSet.new())
fn mapset() -> :ok end.(MapSet.new([1, 2]))
fn mapset([1]) -> :ok end.(MapSet.new([1, 2]))
```
and the following would not
```elixir
fn mapset([1]) -> :ok end.(MapSet.new())
fn mapset([2]) -> :ok end.(MapSet.new([1]))
```
`mapset([])` is the equivalent of `%MapSet{}` and it would match any mapset given.
## Real-world examples
iex> import Kantox.Commons.MapSet
...> now = DateTime.utc_now()
...> checker = fn
...> mapset when is_empty_mapset(mapset) -> :empty_mapset
...> mapset when mapset_size(mapset) == 3 -> :mapset_with_three_elements
...> mapset([^now]) -> :now_mapset
...> %MapSet{} -> :mapset
...> _ -> :unknown
...> end
iex> checker.(MapSet.new([]))
:empty_mapset
iex> checker.(MapSet.new([1, 2, 3]))
:mapset_with_three_elements
iex> checker.(MapSet.new([1, 2]))
:mapset
iex> checker.(mapset([now]))
:now_mapset
iex> checker.(mapset([DateTime.utc_now()]))
:mapset
iex> checker.(nil)
:unknown
The only known drawback would be it would fail with hot upgrades changing `MapSet` version.
"""
@version MapSet.new().version
@doc """
The macro, allowing to retrieve the size of a mapset in guards.
## Examples
iex> import Kantox.Commons.MapSet
...> checker = fn
...> mapset when mapset_size(mapset) == 0 -> :empty_mapset
...> mapset when mapset_size(mapset) < 3 -> :tiny_mapset
...> %MapSet{} -> :huge_mapset
...> end
iex> checker.(MapSet.new([]))
:empty_mapset
iex> checker.(MapSet.new([1]))
:tiny_mapset
iex> checker.(MapSet.new([1, 2, 3, 4, 5]))
:huge_mapset
"""
@doc guard: true
defmacro mapset_size(mapset) do
quote do
is_struct(unquote(mapset), MapSet) and
:erlang.map_get(:version, unquote(mapset)) == unquote(@version) and
map_size(:erlang.map_get(:map, unquote(mapset)))
end
end
@doc """
The guard, allowing checks for an empty mapset.
## Examples
iex> import Kantox.Commons.MapSet
...> checker = fn
...> mapset when is_empty_mapset(mapset) -> :empty_mapset
...> _ -> :unknown
...> end
iex> checker.(MapSet.new([]))
:empty_mapset
iex> checker.(MapSet.new([1]))
:unknown
"""
@doc guard: true
defguard is_empty_mapset(mapset)
when is_struct(mapset, MapSet) and
map_size(:erlang.map_get(:map, mapset)) == 0 and
:erlang.map_get(:version, mapset) == @version
@doc """
The macro, simplifying usage of mapsets in pattern matches.
## Examples
iex> import Kantox.Commons.MapSet
...> now = DateTime.utc_now()
...> checker = fn
...> mapset([1, ^now]) -> :mix_mapset
...> mapset([^now]) -> :now_mapset
...> mapset([]) -> :mapset
...> end
iex> checker.(MapSet.new([3, now, 1]))
:mix_mapset
iex> checker.(mapset([now, 3]))
:now_mapset
iex> checker.(mapset([now]))
:now_mapset
iex> checker.(mapset([3, 2])) # lacking `now` element
:mapset
iex> # fn mapset([var]) -> :ok end
# ** (CompileError) cannot use variable var as map key inside a pattern.
iex> import Kantox.Commons.MapSet
...> mapset([1, 2, 3])
MapSet.new([1, 2, 3])
"""
@doc guard: false
@spec mapset(list()) :: Macro.t()
defmacro mapset(list) do
case __CALLER__.context do
:guard ->
raise_opts = fn description ->
[
file: Path.relative_to_cwd(__CALLER__.file),
line: __CALLER__.line,
description: description
]
end
raise CompileError, raise_opts.("`mapset/1` macro cannot be used in guards")
:match ->
list_ast =
Enum.map(list, fn
{:^, meta, [{_, _, _} = elem]} -> {{:^, meta, [elem]}, []}
# to raise properly, we need the following line
{_var_name, meta, _} = var -> {{:var!, meta, [var]}, []}
elem -> {elem, []}
end)
map_ast = {:%{}, Macro.Env.location(__CALLER__), list_ast}
quote do: %MapSet{version: unquote(@version), map: unquote(map_ast)}
nil ->
quote do: MapSet.new(unquote(list))
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment