Current section
Files
Jump to
Current section
Files
lib/shorter_maps.ex
defmodule ShorterMaps do
@default_modifier_m ?s
@default_modifier_M ?a
@first_letter_uppercase ~r/^\p{Lu}/u
@doc """
Returns a string keyed map with the given keys bound to variables of the same
name.
A common use of `~m` is when working with JSON, which uses exclusively string
keys for its maps. This macro can be used to construct maps from existing
variables, or to destructure a map into new variables:
# Construction:
name = "Chris"
id = 5
~m{name id} # <= %{"name" => "Chris", "id" => 5}
# Pattern Matching
~m{name} = %{"name" => "Bob", "id" => 3}
name # <= "Bob"
See ~M (sigil_M) and the README for extended usages.
"""
defmacro sigil_m(term, modifiers)
defmacro sigil_m({:<<>>, line, [string]}, modifiers) do
do_sigil_m(line, String.split(string), modifier(modifiers, @default_modifier_m), __CALLER__)
end
defmacro sigil_m({:<<>>, _, _}, _modifiers) do
raise ArgumentError, "interpolation is not supported with the ~m sigil"
end
@doc ~S"""
Returns a map with the given keys bound to variables with the same name.
This macro sigil is used to reduce boilerplate when writing pattern matches on
maps that bind variables with the same name as the map keys. For example,
given a map that looks like this:
my_map = %{foo: "foo", bar: "bar", baz: "baz"}
..the following is very common Elixir code:
%{foo: foo, bar: bar, baz: baz} = my_map
foo #=> "foo"
The `~M` sigil provides a shorter way to do exactly this. It splits the given
list of words on whitespace (i.e., like the `~w` sigil) and creates a map with
these keys as the keys and with variables with the same name as values. Using
this sigil, this code can be reduced to just this:
~M{foo bar baz} = my_map
foo #=> "foo"
`~M` can be used in regular pattern matches like the ones in the examples
above but also inside function heads (note the use of `_bar` in this example):
defmodule Test do
import ShortMaps
def test(~M{foo _bar}), do: {:with_bar, foo}
def test(~M{foo}), do: foo
def test(_), do: :no_match
end
Test.test %{foo: "hello world", bar: :ok} #=> {:with_bar, "hello world"}
Test.test %{foo: "hello world"} #=> "hello world"
Test.test %{bar: "hey there!"} #=> :no_match
## Pinning
Matching using the `~M`/`~m` sigils has full support for the pin operator:
bar = "bar"
~M(foo ^bar) = %{foo: "foo", bar: "bar"} #=> this is ok, `bar` matches
foo #=> "foo"
bar #=> "bar"
~M(foo ^bar) = %{foo: "FOO", bar: "bar"} #=> this is still ok
foo #=> "FOO"; since we didn't pin it, it's now bound to a new value
bar #=> "bar"
~M(foo ^bar) = %{foo: "foo", bar: "BAR"} #=> will raise MatchError
## Structs
For using structs instead of plain maps, the first word must be prefixed with
'%':
defmodule Foo do
defstruct bar: nil
end
~M(%Foo bar) = %Foo{bar: 4711}
bar #=> 4711
## Modifiers
The `~m` and `~M` sigils support postfix operators for backwards
compatibility with `ShortMaps`. Atom keys can be specified using the `a`
modifier, while string keys can be specified with the `s` modifier.
~m(blah)a == ~M{blah}
~M(blah)s == ~m{blah}
## Pitfalls
Interpolation isn't supported. `~M(#{foo})` will raise an `ArgumentError`
exception.
The variables associated with the keys in the map have to exist in the scope
if the `~M` sigil is used outside a pattern match:
foo = "foo"
~M(foo bar) #=> ** (RuntimeError) undefined function: bar/0
"""
defmacro sigil_M(term, modifiers)
defmacro sigil_M({:<<>>, line, [string]}, modifiers) do
do_sigil_m(line, String.split(string), modifier(modifiers, @default_modifier_M), __CALLER__)
end
defmacro sigil_M({:<<>>, _, _}, _modifiers) do
raise ArgumentError, "interpolation is not supported with the ~M sigil"
end
defp do_sigil_m(_line, ["%" <> _struct_name | _words], ?s, _caller),
do: raise(ArgumentError, "structs can only consist of atom keys")
defp do_sigil_m(_line, ["%" <> struct_name | words], ?a, caller) do
struct = resolve_module(struct_name, caller)
pairs = make_pairs(words, ?a)
quote do: %unquote(struct){unquote_splicing(pairs)}
end
defp do_sigil_m(line, words, modifier, _caller) do
pairs = make_pairs(words, modifier)
{:%{}, line, pairs}
end
defp resolve_module("__MODULE__", caller) do
{:__MODULE__, [], caller.module}
end
defp resolve_module(struct_name, _caller) do
{:__aliases__, [], [String.to_atom(struct_name)]}
end
defp make_pairs(words, modifier) do
keys = Enum.map(words, &strip_prefix/1)
variables = Enum.map(words, &handle_var/1)
ensure_valid_variable_names(keys)
case modifier do
?a -> keys |> Enum.map(&String.to_atom/1) |> Enum.zip(variables)
?s -> keys |> Enum.zip(variables)
end
end
defp strip_prefix("_" <> name),
do: name
defp strip_prefix("^" <> name),
do: name
defp strip_prefix(name),
do: name
defp handle_var("^" <> name) do
{:^, [], [Macro.var(String.to_atom(name), nil)]}
end
defp handle_var(name) do
String.to_atom(name) |> Macro.var(nil)
end
defp modifier([], default), do: default
defp modifier([mod], _default) when mod in 'as', do: mod
defp modifier(_, _default) do
raise(ArgumentError, "only these modifiers are supported: s, a")
end
defp ensure_valid_variable_names(keys) do
Enum.each keys, fn k ->
unless k =~ ~r/\A[a-zA-Z_]\w*\Z/ do
raise ArgumentError, "invalid variable name: #{k}"
end
end
end
end