Field punning in Elixir via a shorthand for constructing tagged two-tuple variable references.
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.
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]
]
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}
]
}
}
]
}
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
|
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.
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:
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...
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)
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.
We often use Keyword
lists and Map
s 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?
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
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
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.
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 andMap
s - We have two different common key types,
Atom
s andString
s - 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.
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.