Created
February 4, 2023 09:42
-
-
Save am-kantox/03325197ff9608dfa83457dbe0d144e9 to your computer and use it in GitHub Desktop.
`MapSet` matchers/guards
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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