# TaggedTupleShorthand
<!-- MODULEDOC BLURB -->
> **_Field punning in Elixir via a shorthand for constructing tagged two-tuple variable references._**
<!-- MODULEDOC BLURB -->
[![Version][hex-pm-version-badge]][hex-pm-versions]
[![Documentation][docs-badge]][docs]
[![License][hex-pm-license-badge]][hex-pm-package]
[![Dependencies][deps-badge]][deps]
## Setup
### Installation
`TaggedTupleShorthand` is distributed via [hex.pm][hex-pm], you can install it with your dependency manager of choice using the config provided on its [hex.pm package][hex-pm-package] listing.
<!-- MODULEDOC EXTRA -->
<!--
all hyperlinks within this snippet must be inline,
rather than using markdown link references
-->
### 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:
```elixir
# 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:
```elixir
# project/.credo.exs
%{
configs: [
%{
name: "default",
checks: %{
disabled: [
{Credo.Check.Readability.ModuleAttributeNames, false}
]
}
}
]
}
```
<!-- MODULEDOC EXTRA -->
## Usage
<!-- MODULEDOC USAGE -->
<!--
all hyperlinks within this snippet must be inline,
rather than using markdown link references
-->
### 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](https://groups.google.com/g/elixir-lang-core/c/NoUo2gqQR3I/m/ddgTD3DU4oMJ):
```elixir
def handle_in(
event,
%{
"chat" => chat,
"question_id" => question_id,
"data" => data,
"attachment" => attachment
},
socket
)
when is_binary(chat) do...
```
After:
```elixir
def handle_in(event, %{@"chat", @"question_id", @"data", @"attachment"}, socket)
when is_binary(chat) do...
```
Diff:
```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](https://github.com/fly-apps/live_beats/blob/ac9780472e7019af274110a1cf71250a8d40c986/lib/live_beats_web/controllers/file_controller.ex#L11-L20):
```elixir
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:
```elixir
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:
```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)
```
<!-- MODULEDOC USAGE -->
## Motivation
<!-- MODULEDOC ABOUT -->
<!--
all hyperlinks within this snippet must be inline,
rather than using markdown link references
-->
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](https://dev.realworldocaml.org/records.html) — OCaml
- [Record Puns](https://ghc.gitlab.haskell.org/ghc/doc/users_guide/exts/record_puns.html) — Haskell
- [Object Property Value Shorthand](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Object_initializer#property_definitions) — ES6 Javascript
- [Hash Key Pattern Matching](https://docs.ruby-lang.org/en/3.0/syntax/pattern_matching_rdoc.html#label-Matching+non-primitive+objects-3A+deconstruct+and+deconstruct_keys) — 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:
```elixir
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:
```elixir
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[‡](https://en.wikipedia.org/wiki/Citation_needed), the key itself makes for a fine variable name:
```elixir
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:
```elixir
[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:
```js
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:
```rb
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 `Map`s
- We have two different common key types, `Atom`s and `String`s
- 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.
<!-- MODULEDOC ABOUT -->
## 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][test-matrix] to see if it will work for your combination.
<!-- LINKS & IMAGES -->
<!-- Hex -->
[hex-pm]: https://hex.pm
[hex-pm-package]: https://hex.pm/packages/tagged_tuple_shorthand
[hex-pm-versions]: https://hex.pm/packages/tagged_tuple_shorthand/versions
[hex-pm-version-badge]: https://img.shields.io/hexpm/v/tagged_tuple_shorthand.svg?cacheSeconds=86400&style=flat-square
[hex-pm-downloads-badge]: https://img.shields.io/hexpm/dt/tagged_tuple_shorthand.svg?cacheSeconds=86400&style=flat-square
[hex-pm-license-badge]: https://img.shields.io/badge/license-MIT-7D26CD.svg?cacheSeconds=86400&style=flat-square
<!-- Docs -->
[docs]: https://hexdocs.pm/tagged_tuple_shorthand/index.html
<!-- [docs-guides]: https://hexdocs.pm/tagged_tuple_shorthand/usage.html#content -->
[docs-badge]: https://img.shields.io/badge/documentation-online-purple?cacheSeconds=86400&style=flat-square
<!-- Deps -->
[deps]: https://hex.pm/packages/tagged_tuple_shorthand
[deps-badge]: https://img.shields.io/badge/dependencies-0-blue?cacheSeconds=86400&style=flat-square
<!-- Benchmarks -->
<!-- [benchmarks]: https://christhekeele.github.io/tagged_tuple_shorthand/bench -->
<!-- [benchmarks-badge]: https://img.shields.io/badge/benchmarks-online-2ab8b5?cacheSeconds=86400&style=flat-square -->
<!-- Contributors -->
<!-- [contributors]: https://hexdocs.pm/tagged_tuple_shorthand/contributors.html -->
<!-- [contributors-badge]: https://img.shields.io/badge/contributors-%F0%9F%92%9C-lightgrey -->
<!-- Status -->
[suite]: https://github.com/christhekeele/tagged_tuple_shorthand/actions?query=workflow%3A%22Test+Suite%22
<!-- [coverage]: https://coveralls.io/github/christhekeele/tagged_tuple_shorthand -->
<!-- Release Status -->
[release]: https://github.com/christhekeele/tagged_tuple_shorthand/tree/release
[release-suite]: https://github.com/christhekeele/tagged_tuple_shorthand/actions?query=workflow%3A%22Test+Suite%22+branch%3Arelease
[release-suite-badge]: https://img.shields.io/github/actions/workflow/status/christhekeele/tagged_tuple_shorthand/test-suite.yml?branch=release&cacheSeconds=86400&style=flat-square
<!-- [release-coverage]: https://coveralls.io/github/christhekeele/tagged_tuple_shorthand?branch=release -->
<!-- [release-coverage-badge]: https://img.shields.io/coverallsCoverage/github/christhekeele/tagged_tuple_shorthand?branch=release&cacheSeconds=86400&style=flat-square -->
<!-- Latest Status -->
[latest]: https://github.com/christhekeele/tagged_tuple_shorthand/tree/latest
[latest-suite]: https://github.com/christhekeele/tagged_tuple_shorthand/actions?query=workflow%3A%22Test+Suite%22+branch%3Alatest
[latest-suite-badge]: https://img.shields.io/github/actions/workflow/status/christhekeele/tagged_tuple_shorthand/test-suite.yml?branch=latest&cacheSeconds=86400&style=flat-square
<!-- [latest-coverage]: https://coveralls.io/github/christhekeele/tagged_tuple_shorthand?branch=latest -->
<!-- [latest-coverage-badge]: https://img.shields.io/coverallsCoverage/github/christhekeele/tagged_tuple_shorthand?branch=latest&cacheSeconds=86400&style=flat-square -->
<!-- Other -->
<!-- [changelog]: https://hexdocs.pm/tagged_tuple_shorthand/changelog.html -->
[test-matrix]: https://github.com/christhekeele/tagged_tuple_shorthand/actions/workflows/test-matrix.yml
<!-- [test-edge]: https://github.com/christhekeele/tagged_tuple_shorthand/actions/workflows/test-edge.yml -->
<!-- [contributing]: https://hexdocs.pm/tagged_tuple_shorthand/contributing.html -->