tagged_tuple_shorthand

Field punning for Elixir via a tagged 2-tuple variable reference macro


License
Other

Documentation

TaggedTupleShorthand

Field punning in Elixir via a shorthand for constructing tagged two-tuple variable references.

Version Documentation License Dependencies

Setup

Installation

TaggedTupleShorthand is distributed via hex.pm, you can install it with your dependency manager of choice using the config provided on its hex.pm package listing.

Formatting

At time of writing, this library does not do any custom formatting, but that will likely change. To get support for it on release, you can add :tagged_tuple_shorthand to your formatter options' :import_deps today, ex:

# project/.formatter.exs
[
  import_deps: [:tagged_tuple_shorthand]
]

Linting

At time of writing, Credo is reasonably upset by how we re-appropriate the module attribute operator. We may offer a replacement check in the future, but for now you should disable the Credo.Check.Readability.ModuleAttributeNames check in your configuration, ex:

# project/.credo.exs
%{
  configs: [
    %{
      name: "default",
      checks: %{
        disabled: [
          {Credo.Check.Readability.ModuleAttributeNames, false}
        ]
      }
    }
  ]
}

Usage

Basic Usage

TaggedTupleShorthand overrides the @ operator to accept a literal atom or string, that turns into a tagged two-tuple variable reference at compile-time:

Form Expands To
@:atom {:atom, atom}
@^:atom {:atom, ^atom}
@"string" {"string", string}
@^"string" {"string", ^string}
@anything_else Fallback to Kernel.@/1

Examples

iex> use TaggedTupleShorthand
iex> foo = 1
iex> @:foo
{:foo, 1}
iex> @:foo = {:foo, 2}
{:foo, 2}
iex> foo
2
iex> @^:foo = {:foo, 2}
iex> @^:foo = {:foo, 3}
** (MatchError) no match of right hand side value: {:foo, 3}

This is not the most useful construct, until we start to use it in destructuring.

Field Punning Usage

As it so happens, this tagged two-tuple variable reference shorthand expands at compile-time to AST that gives us field punning. Just use @:atom and @"string" when destructuring:

iex> use TaggedTupleShorthand
iex> destructure_map = fn %{@:foo, @"bar"} ->
...>   {foo, bar}
...> end
iex> map = %{"bar" => 2, foo: 1}
iex> destructure_map.(map)
{1, 2}

Some more realistic examples:

In Phoenix Channels

Before:

def handle_in(
      event,
      %{
        "chat" => chat,
        "question_id" => question_id,
        "data" => data,
        "attachment" => attachment
      },
      socket
    )
    when is_binary(chat) do...

After:

def handle_in(event, %{@"chat", @"question_id", @"data", @"attachment"}, socket)
    when is_binary(chat) do...

Diff:

-def handle_in(
-      event,
-      %{
-        "chat" => chat,
-        "question_id" => question_id,
-        "data" => data,
-        "attachment" => attachment
-      },
-      socket
-    )
+def handle_in(event, %{@"chat", @"question_id", @"data", @"attachment"}, socket)
     when is_binary(chat) do...

In Phoenix Controller Actions

Before:

def show(conn, %{"id" => id, "token" => token}) do
  case Phoenix.Token.decrypt(conn, "file", token, max_age: :timer.minutes(1)) do
    {:ok, %{id: ^id, vsn: 1, size: _size}} ->
     path = MediaLibrary.local_filepath(id)
     do_send_file(conn, path)

    _ ->
      send_resp(conn, :unauthorized, "")
  end
end

After:

def show(conn, %{@"id", @"token"}) do
  case Phoenix.Token.decrypt(conn, "file", token, max_age: :timer.minutes(1)) do
    {:ok, %{@^:id, vsn: 1, size: _size}} ->
     path = MediaLibrary.local_filepath(id)
     do_send_file(conn, path)

    _ ->
      send_resp(conn, :unauthorized, "")
  end
end

Diff:

-def show(conn, %{"id" => id, "token" => token}) do
+def show(conn, %{@"id", @"token"}) do
   case Phoenix.Token.decrypt(conn, "file", token, max_age: :timer.minutes(1)) do
-    {:ok, %{id: ^id, vsn: 1, size: _size}} ->
+    {:ok, %{@^:id, vsn: 1, size: _size}} ->
      path = MediaLibrary.local_filepath(id)
      do_send_file(conn, path)

Motivation

What is field punning? It's a common form of syntactic sugar you may already be familiar with from other languages. It goes by many names:

We'll stick with "field punning" throughout this explanation.

Background

We often use Keyword lists and Maps to associate values with a given key:

list = [foo: 1, bar: 2]
map = %{fizz: 3, buzz: 4}

Often, we want to get values of interest associated with a given key out of an associative data structure. There are functions as well as syntax sugar for this already:

Keyword.get(list, :foo) #=> 1
list[:bar] #=> 2
map[:fizz] #=> 3
map.buzz #=> 4

If we're interested in a value, we are probably going to assign it to a variable. What's a good name for that variable? 94% of the time‡, the key itself makes for a fine variable name:

foo = Keyword.get(list, :foo)
bar = list[:bar]
fizz = map[:fizz]
buzz = map.buzz

And thanks to the glory of pattern matching, we can express this with destructuring:

[foo: foo, bar: bar] = list
%{fizz: fizz, buzz: buzz} = map
foo #=> 1
bar #=> 2
fizz #=> 3
buzz #=> 4

This begs the question: if this is so common, why do we have to type out the same name twice, once to name the key, and again to name the variable, when destructuring?

In Javascript

You can do this destructuring of key/value pairs into matching variable names by assigning to a "barewords" style object literal:

data = {foo: 1, bar: 2, baz: 3}
//=> {foo: 1, bar: 2, baz: 3}
{foo, bar} = data
foo //=> 1
bar //=> 2

In Ruby

You can do this destructuring of key/value pairs into matching variable names by pattern matching into a "keywords" style hash literal:

data = {foo: 1, bar: 2, baz: 3}
#=> {:foo=>1, :bar=>2, :baz=>3}
data => {foo:, bar:}
foo #=> 1
bar #=> 2

Benefits

That is what field punning is: a short-hand syntactic sugar for deconstruction of key/value pairs in associative data structures, interacting with variable names in the current scope. It is popular for several reasons:

  • This syntax saves on visual noise, expressing destructuring key/value data tersely in the common case of the key making for a sufficient variable name.
  • This syntax calls attention to the cases where we are intentionally not re-using the key as a variable name, placing emphasis on a subtle decision a developer decided was important for readability or understanding.
  • This syntax prevents common typos, and ensures that variable names match keys throughout refactors when that is the desired behaviour.

In Elixir

An Elixir implementation of field punning has to work in several more scenarios than other languages, since:

  • We have two different common associative data structures, Keyword lists and Maps
  • We have two different common key types, Atoms and Strings
  • We have two different common syntaxes for key/value associativity, arbitrary => value (maps only) and atom: value (atom keys only)

This particular macro for tagged two-tuple variable references gets us just that.

Supported Versions

TaggedTupleShorthand is tested against many combinations of Elixir and OTP, and this syntax only works from Elixir v1.17.0 and onwards. Check the latest test matrix run to see if it will work for your combination.