ShorterMaps v1.2.0 ShorterMaps

{:shorter_maps, "~> 1.2"},

~M sigil for map shorthand. ~M{a} ~> %{a: a}

New Features

v1.2

  • Added support for map update syntax (~M{old_map|first_name last_name}), instead of writing %{old_map|first_name: first_name, last_name: last_name}. Works with both ~m and ~M.

v1.1

  • Added support for leading underscore variables (~M{_id name} = person), which allows specifying structural requirements while minimizing compiler warnings for unused variables.

Motivation

Code like %{id: id, name: name, address: address} occurs with high frequency in many programming languages. In Elixir, additional uses occur as we pattern match to destructure existing maps.

ES6 (ES2015 for those folks who insist on proper names) provided javascript with a shorthand to create maps with keys inferred by variable names, and allowed destructuring those maps into variables named for the keys. ShorterMaps provides that functionality to Elixir.

Credits

ShorterMaps adds additional features to the original project, ShortMaps, located here. The reasons for the divergence are summarized here.

The key syntactic difference is motivated by the trailing a in ~m{}a. To maintain backward compatibility, that syntax still works, but ShorterMaps adds a ~M sigil that defaults to the a modifier.

Basic Usage

Note: you must import ShorterMaps for the sigil to work.

Pattern Matching / Function Heads

iex> import ShorterMaps
...> ~M{foo bar baz} = %{foo: 1, bar: 2, baz: 3}
...> foo
1
...>
...> defmodule MyMod do
...>   def extract_id(~M{id} = args), do: id
...> end
...> MyMod.extract_id(%{id: 5, name: "Chris"})
5

end

Map Construction

iex> import ShorterMaps
...> name = "Meg"
...> ~M{name} # M = atom keys
%{name: "Meg"}
...> ~m{name} # m = String keys
%{"name" => "Meg"}

Structs

The first word inside the sigil must be ‘%’ followed by the module name:

iex> import ShorterMaps
...> defmodule MyStruct do
...>   defstruct [id: 0, name: ""]
...> end
...>
...> # Struct construction:
...> id = 5
...> name = "Chris"
...> ~M{%MyStruct id name}
%MyStruct{id: 5, name: "Chris"}
...>
...> # Pattern Matching:
...> ~M{%MyStruct id} = %MyStruct{id: 1, name: "Bob"}
...> id
1

Variable Pinning

iex> import ShorterMaps
...> name = "Meg"
...> ~M{^name} = %{name: "Meg"}
%{name: "Meg"}
...> ~M{^name} = %{name: "Megan"}
** (MatchError) no match of right hand side value: %{name: "Megan"}

Variable Ignore

Useful for pattern matching against the structure of a map when you don’t need all of the variables.

iex> import ShorterMaps
...> ~M{_foo bar} = %{foo: "bar", bar: "foo"}
%{bar: "foo", foo: "bar"}
...> bar
"foo"
...> foo
** (CompileError) iex:4: undefined function foo/0

Map Update Syntax


iex> import ShorterMaps
...> record = %{first_name: "chris", last_name: "meyer", id: 1}
...> [first_name, last_name] = ["Chris", "Meyer"]
...> updated = ~M{record|first_name last_name}
%{first_name: "Chris", id: 1, last_name: "Meyer"}

You can see more examples in the docs for the sigil_M/sigil_m macros.

Installation

# mix.exs

defp deps do
  [
    {:shorter_maps, "~> 1.2"},
  ]
end

Summary

Macros

Returns a map with the given keys bound to variables with the same name

Returns a string keyed map with the given keys bound to variables of the same name

Macros

sigil_M(term, modifiers)

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
sigil_m(term, modifiers)

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.