Andrew Timberlake Andrew Timberlake

Hi, I’m Andrew, a programmer and entrepreneur from South Africa,
building Mailcast for taking control of your email.
Thanks for visiting and reading.


Shorthand maps for Elixir

Shorthand is the library I never “released”.
I created the library six years ago and have used it in every (private) project since. But I never promoted it. This week I made some updates to make things a little easier and I thought I’d share it with the world.

What is Shorthand

Shorthand is an Elixir library that provides convenient syntax for working with maps and keyword lists.

The library allows you to create maps where the name of the key is the same as the variable holding its value.
This is very similar to Javascript’s shorthand property names for objects. {foo} is shorthand for {foo: foo}.
If it were a feature of Elixir, it might look like this:

name = "Andrew"
age = 45

# Invalid Elixir
%{name, age} #=> %{name: name, age: age}

But the proposal didn’t get accepted.

One of the challenges with the proposal was how to deal with the difference between atom and string based keys, which I’ll explain how Shorthand handles a little later.

A couple of libraries were produced that tried to solve the problem. All of them used Sigils. The problem I saw with Sigils is that they are string based. The Sigil is a macro that receives a string that needs to be parsed and turned into valid Elixir. This isn’t a great experience when working in a code editor where you’re working with multiple “variables”.

Instead of Sigils, Shorthand uses macros.

import Shorthand

name = "Andrew"
age = 45

m(name, age)
# => %{name: name, age: age}

Because of the use of macros, your editor will see each entry as a variable and highlight it correctly. An added benefit is that Elixir reports syntax errors at the variable level. Code completion also works out of the box.

What can you use it for?

Shorthand can be used anywhere you use a map which includes map building, but also pattern matching.

It’s great in tests when you have a lot of information being passed in from a setup. m/? is the macro for atom based maps.

test "testing something", m(conn, account, user, company, client, whatever) do
  # ...
end

# Typical Elixir would be
test "testing something", %{
  conn: conn,
  account: account,
  user: user,
  company: company,
  client: client,
  whatever: whatever} do
  # ...
end

It can be very handy when processing a lot of parameters passed from a form. sm/? is the macro for string based maps.

def create(conn, sm(user: sm(first_name, last_name, email, cellphone))) do
  # ...

end

# Typical Elixir would be
def create(conn, %{
  "user" => %{
    "first_name" => first_name,
    "last_name" => last_name,
    "email" => email,
    "cellphone" => cellphone,
  }
}) do
  # ...
end

It is very useful for quickly changing a string based map to an atom based map.

sm(user = sm(first_name, last_name, email, cellphone)) = params
user = m(first_name, last_name, email, cellphone)
# => %{first_name: first_name, last_name: last_name, email: email, cellphone: cellphone}

It is quite versatile because it is not only useful for maps where the key is the same as the variable holding its value. It can also include other keys with named variables

m(company, account, current_user: user) = params
# => %{company: company, account: account, current_user: user}

Using a little bit of creative magic, you can also assign a variable directly based on a key (which I snuck into an example above).

sm(user = sm(first_name, last_name, email, cellphone)) = params
# is actually the same as
%{"user" =>
  %{
    "first_name" => first_name,
    "last_name" => last_name,
    "email" => email,
    "cellphone" => cellphone
  } = user
} = params

Not only maps

Shorthand also has love for Keyword lists (kw/?).

kw(first_name, last_name, email, cellphone)
# is the same as
[first_name: first_name, last_name: last_name, email: email, cellphone: cellphone]

And finally, though not nearly as useful, it supports Structs (st/?).

st(User, first_name, last_name, email, cellphone)
# is the same as
%User{first_name: first_name, last_name: last_name, email: email, cellphone: cellphone}

Pattern matching

Shorthand supports pattern matching on maps, keyword lists, and structs.

m(^first_name) = user
# is the same as
%{first_name: ^first_name} = user
def create(conn, sm(^first_name, _last_name)) do
  # ...
end
# is the same as
def create(conn, %{first_name: ^first_name, last_name: _last_name}) do
  # ...
end

You can find a full list of the expansion equivalents in the docs.

Notes

Because Shorthand uses macros, it needs to be included in your project using import Shorthand.
This can be done at the top of each file, or for tests in a test helper, or in Phoenix projects, in the the MyAppWeb module helpers.

By default, Shorthand supports up to 10 variables (i.e. m/1 to m/10). But should you need more, you can configure it using a compile time option:

config :shorthand, variable_args: 20

You can also have an unlimited number of variables by calling the macros with a list of variables.

m([first_name, last_name, email, cellphone])
# is the same as
%{first_name: first_name, last_name: last_name, email: email, cellphone: cellphone}

Conclusion

Shorthand is a simple library that I’ve found very useful in my projects.
It’s the first library that I install as soon as I start a new project.
I hope you find it useful too.

1 Nov 2024