View Source TaggedTupleShorthand (TaggedTupleShorthand v0.0.1)

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

use TaggedTupleShorthand

When you use TaggedTupleShorthand, you are replacing Kernel.@/1 with:

  • an overloaded TaggedTupleShorthand.@/1 implementation
  • that supports @:atom and @"string" tagged tuple variable references
  • and otherwise falls back to normal @module_attribute semantics

Field Punning

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.

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:

FormExpands To
@:atom{:atom, atom}
@^:atom{:atom, ^atom}
@"string"{"string", string}
@^"string"{"string", ^string}
@anything_elseFallback 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)

Extras

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}
        ]
      }
    }
  ]
}

Summary

Functions

Generates tagged two-tuple variable references from atom and string literals.

Functions

@literal

(macro)

Generates tagged two-tuple variable references from atom and string literals.

Otherwise falls back to Kernel.@/1:

FormExpands To
@:atom{:atom, atom}
@^:atom{:atom, ^atom}
@"string"{"string", string}
@^"string"{"string", ^string}
@anything_elseFallback 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}

Intended to be used in pattern matching constructs to enable field punning, see the module documentation for an explanation of field punning and its intended usage.