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:
- 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 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?
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 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.
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
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.
Otherwise falls back to Kernel.@/1
:
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}
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.