TaggedTupleShorthand
Field punning in Elixir via a shorthand for constructing tagged two-tuple variable references.
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
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
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
endAfter:
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
endDiff:
-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:
- Field Punning — OCaml
- Record Puns — Haskell
- Object Property Value Shorthand — ES6 Javascript
- Hash Key Pattern Matching — Ruby
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 #=> 4If 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.buzzAnd 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 #=> 4This 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 //=> 2In 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 #=> 2Benefits
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,
Keywordlists andMaps -
We have two different common key types,
Atoms andStrings -
We have two different common syntaxes for key/value associativity,
arbitrary => value(maps only) andatom: 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.