christhekeele

christhekeele

Hey all!

I’m releasing TaggedTupleShorthand today as an experiment with Elixir syntax.

Field Punning

The aim of the library is to support a commonly-requested Elixir feature1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17:

Short-hand for constructing, deconstructing, and pattern matching on key/value pairs in associative data structures, interacting with variable names in the current scope.

This is called “field punning”—essentially trying to turn this

def handle_in(
      event,
      %{
        "chat" => chat,
        "question_id" => question_id,
        "data" => data,
        "attachment" => attachment
      },
      socket
    )
    when is_binary(chat) do...

into this:

def handle_in(event, %{chat, question_id, data, attachment}, socket)
    when is_binary(chat) do...

This syntax sugar exists with many variations in many other languages. 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.

Generally speaking, Elixir’s pattern matching and destructuring functionality greatly surpasses other languages, so people are often surprised to discover that no syntax for field punning exists!

Why not?

Well, for a lot of reasons. I encourage you to explore discussions around previous proposals, but in my opinion it boils down to:

  • 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)
  • We have another common container type, Tuples, that look a lot like Maps, and many proposed syntaxes blur the line between them visually
  • It is hard to invent a syntax that spans all of these considerations consistently
  • Everyone has a different, incompatible preference as to which permutations of these considerations to support and which to ignore completely
  • Consensus is hard, syntax sugar is subjective, and a good compromise leaves everyone dissatisfied
  • Therefore, a working field punning proposal for the language that satisfies everyone has remained elusive

Anyways, that’s field punning…

...and now, for something completely different.


Tagged Tuple Shorthand

“Tagged Tuples” describe a pattern where you have a value, and want to “tag” it with information as to what it represents. In Elixir, this is generally accomplished with a two-tuple, where the first element of the tuple is an atom “tag”, and the second element is the value itself:

case try_to_procure_an_integer() do
  result when is_integer(result) -> {:ok, result}
  other ->
    reason = "not an integer: #{inspect other}"
    {:error, reason}
end

Here, {:ok, result} and {:error, reason} are the tagged tuples. This idiom is overwhelmingly common across the ecosystem; you’ve probably wrote similar Elixir code yourself before you learned the term for it.

What if there was a way of constructing, deconstructing, and pattern matching on tagged tuples that tied into variable names in the current scope? An operator for it, even?

Introducing TaggedTupleShorthand.@/1

Let’s devise an operator that takes either an atom or a string, and expands at compile-time to a tagged tuple, with a variable reference mirroring that atom/string as the second element. We’ll use the unary @ operator since it is already overridable via Elixir macros, and has the correct operator arity, precedence, and associativity for this use-case.

use TaggedTupleShorthand

foo = 1
@:foo # aka {:foo, foo}
#=> {:foo, 1}

bar = 2
@"bar" # aka {"bar", bar}
#=> {"bar", 2}

Essentially,

Form Expands To
@:atom {:atom, atom}
@^:atom {:atom, ^atom}
@"string" {"string", string}
@^"string" {"string", ^string}
@anything_else Fallback to Kernel.@/1

This is… certainly a thing? A novel bit of syntax, hard to acclimate to, but perhaps justifiable in a language with such widespread idiomatic usage of tagged two-tuples?

We could refactor the previous example by choosing our variable names carefully:

use TaggedTupleShorthand

case try_to_procure_an_integer() do
  ok when is_integer(ok) ->
    @:ok # {:ok, ok}
  other ->
    error = "not an integer: #{inspect other}"
    @:error # {:error, error}
end

We could even craft string-tagged tuples, for some reason, why not:

case try_to_procure_an_integer() do
  ok when is_integer(ok) ->
    @"ok" # {"ok", ok} <- nobody wants this
  other ->
    error = "not an integer: #{inspect other}"
    @"error" # {"error", error} <- get this right out of here
end

But why?

I’m sure you see where this is going.


Tagged Tuple Shorthand as Field Punning

It turns out, this simple TaggedTupleShorthand.@/1 macro is all we need to get field punning in Elixir, with expressive power inline with our robust pattern matching and exceeding other languages’ capabilities.

The Abstract Syntax Tree for Elixir—its AST that macros can operate on—already represents key/value pairs in map and keyword list literals as tagged tuples. That is, in any literal map or list we might want to use field punning in, we can use a macro that expands into a tagged tuple and a variable reference, without losing any other expressive powers of Elixir’s pattern matching syntax. A macro like TaggedTupleShorthand.@/1.

Put another way, if we can acclimate to this:

foo = 1
@:foo
#=> {:foo, 1}

and learn to swallow/write a linter to forbid this:

ok when is_integer(ok) ->
  @"ok" # {"ok", ok} <- nobody wants this

We can get rich field punning in places like these, and anywhere else two-tuples are used in Elixir AST:

destructure_map = fn %{@:foo, @"bar"} ->
  {foo, bar}
end

map = %{"bar" => 2, foo: 1}
destructure_map.(map)
#=> {1, 2}

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...

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)

What do you think?

Have you ever wanted field punning in Elixir? Is it worth introducing a general tagged tuple syntax to get it?

Maybe... But not by enough

Worth noting that repurposing the module attribute operator @ for field punning is not my preference, nor do I encourage the use of the tagged tuple shorthand outside of field punning—but this is the only way to deliver this feature in library form.

TaggedTupleShorthand is compatible with normal module attribute usage, so you can use it everywhere and still @doc everything to your heart’s content. If this gains popularity and merits a language proposal, we can always introduce a new distinct unary operator as needed and restrict the scope of where this expression is allowed.

Try it out!

Grab the library and play with it in your codebase or console today!

I’m still not sure how I feel about this syntax myself, but now that I have a library as a proof-of-concept, I’m going to start playing with it in personal projects, and I invite you to do the same.

Well then, I'm going to put them on and make you watch me have fun. See?

https://github.com/christhekeele/tagged_tuple_shorthand

Showing Posts 1 to 3

cmo

cmo

I don’t mind it for de-structuring maps but you lost me with the pin syntax and magic tuples.

:nauseated_face:

I wish we could use the dot syntax through the function without paying the performance penalty for using Access.

christhekeele

christhekeele OP

Yep, it’s not really intended to be used generally for “magic” two-tuples wherever. But I am laying the mechanism behind how it enables map destructuring bare, intentionally. Without first-class language support to reign in the scope of where it’s allowed, and improving match and variable warnings/errors accordingly, at the end of the day it’s just a magic two-tuple syntax.

That’s kind of the idea behind naming it use TaggedTupleShorthand. I want people trying this syntax on to understand the underlying mechanism when they sign up for it, because without first-class language support you’re going to see confusing messages and have to think a little more about the ordering of expressions pattern matching in certain edge-cases.

(Also, as this is an experiment, I don’t want to squat on a more valuable library name that someone’s likely to want in the future. This is exactly what it says: a tagged tuple shorthand! It just gets us terse k/v destructuring by merit of how Elixir map literals work under the hood.)

Eiji

Eiji

How about something like that?

defmodule MyLib do
  defmacro sigil_p({:<<>>, _meta, [string]}, flags) do
    left_to_atom = ?a in flags

    case String.split(string, ":", parts: 2) do
      ["^" <> left] -> decompose_pun(left, left_to_atom, true)
      [left] -> decompose_pun(left, left_to_atom, false)
      [left, "^" <> right] -> decompose_pun(left, right, left_to_atom, true)
      [left, right] -> decompose_pun(left, right, left_to_atom, false)
    end
  end

  defp decompose_pun(left, left_to_atom, right_to_pin) do
    left
    |> ast_left(left_to_atom)
    |> then(&ast_decompose_pun(&1, &1, right_to_pin))
  end

  defp decompose_pun(left, right, left_to_atom, right_to_pin) do
    left
    |> ast_left(left_to_atom)
    |> then(&ast_decompose_pun(&1, right, right_to_pin))
  end

  defp ast_left(left, true), do: String.to_existing_atom(left)
  defp ast_left(left, false), do: left

  defp ast_decompose_pun(left, right, true), do: {left, ast_pin_variable(right)}
  defp ast_decompose_pun(left, right, false), do: {left, ast_variable(right)}

  defp ast_pin_variable(right), do: {:^, [], [ast_variable(right)]}

  defp ast_variable(right) when is_atom(right), do: Macro.var(right, nil)
  defp ast_variable(right), do: right |> String.to_existing_atom() |> ast_variable()
end

defmodule Example do
  import MyLib

  def with_pin do
    atom = :old_atom
    string = "old string"
    result = "Old Result"
    value = result

    ~p"^atom"a = {:atom, :old_atom}
    %{~p"^atom"a} = %{atom: :old_atom}
    [~p"^atom"a] = [atom: :old_atom]

    ~p"^string" = {"string", "old string"}
    %{~p"^string"} = %{"string" => "old string"}
    [~p"^string"] = [{"string", "old string"}]

    ~p"ok:^result"a = {:ok, "Old Result"}
    %{~p"key:^value"a} = %{key: "Old Result"}
    [~p"key:^value"a] = [key: "Old Result"]

    result = :new_result
    value = result
    ~p"ok:^result" = {"ok", :new_result}
    %{~p"key:^value"} = %{"key" => :new_result}
    [~p"key:^value"] = [{"key", :new_result}]
  end

  def without_pin do
    ~p"atom"a = {:atom, :atom}
    true = atom == :atom

    %{~p"atom"a} = %{atom: :atom}
    true = atom == :atom

    [~p"atom"a] = [atom: :atom]
    true = atom == :atom

    ~p"string" = {"string", "string"}
    true = string == "string"

    %{~p"string"} = %{"string" => "string"}
    true = string == "string"

    [~p"string"] = [{"string", "string"}]
    true = string == "string"

    ~p"ok:result"a = {:ok, 5}
    true = result == 5

    ~p"ok:result" = {"ok", 10}
    true = result == 10

    %{~p"key:value"a} = %{key: 5}
    true = value == 5

    %{~p"key:value"} = %{"key" => 10}
    true = value == 10

    [~p"key:value"a] = [key: 5]
    true = value == 5

    [~p"key:value"] = [{"key", 10}]
    true = value == 10
  end
end

Example.with_pin()
Example.without_pin()
— All posts loaded —

Where Next? Top

Trending in Announcing Top

restlessronin
The repo is at GitHub - cyberchitta/openai_ex: Community maintained Elixir library for OpenAI API · GitHub. Docs are at OpenaiEx User Gu...
152 12366 136
New
type1fool
WebAuthnLiveComponent WebAuthnComponents See this post about renaming the package. Passwordless authentication for Phoenix LiveView app...
New
mudasobwa
I fully migrated to my own harness from Anthropic/Gemini and I think it’s time to share it. Welcome DSH, the DeepSeek Harness, fully writ...
New
ausimian
Castle is a package I put together to assist with building Elixir releases capable of being live-upgraded via the ‘official’ release hand...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
ausimian
Emily is an Elixir library that runs Nx computations on Apple’s MLX. Install it as the default Nx backend and Nx, defn, Axon, Nx.Serving,...
New
kip
In 2021 I started a new library called Tempo with the objective of modelling time as a set of intervals - not as instants. In 2022 I gave...
New

Other Trending Topics Top

budgie
A little off-topic, but I feel like people here have a good head on their shoulders. I used to be quite good at making software. Was luc...
New
KristerV
Hey. Is there anyone here who creates agents in their apps? Not talking about using agents, but creating them. I’m finding it pretty diff...
New
juhalehtonen
There has been a thread to discuss the Stack Overflow Developer Survey on this forum every year since 2018, so here’s yet another one for...
New
budgie
I love Elixir. It’s one of 2 programming languages I’ve ever fallen in love with. But I don’t use it anymore. Serverless was the promis...
New
OndrejValenta
(I just needed to vent somewhere and LinkedIn is full of hope, or hype, I’m not sure which exactly) AI dream has many faces, but general...
New
Null-logic-0
What IDE or editor are you using for Elixir development? Personally, I use Zed, and I really like it, but sometimes I wish there were a ...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews