Why is the syntax tree for a 2-tuple different than for other tuples?

Because Elixir code is parsed into 3-tuples, there needs to be a different notation for an embedded 3-tuple. It is what I think of as “constructor” notation:

iex(12)> quote do: {1, 2, 3}
{:{}, [], [1, 2, 3]}

That’s true for other tuple lengths, which makes sense: why not be consistent?

Except for 2-tuples, which are represented unchanged:

iex(7)> quote do: {1, 2}
{1, 2}

My hunch is that’s because lists are unchanged:

iex(8)> quote do: [1, 2, 3]
[1, 2, 3]

… and making 2-tuples unchanged means that keyword lists are also unchanged:

iex(11)> quote do: [a: 1, b: 2]         
[a: 1, b: 2]

I can’t think of a case where treating 2-tuples like other tuples would cause ambiguity, though. Is this just to make literal keyword lists more readable?


Maps also have a constructor representation:

iex(13)> quote do: %{a: 1}
{:%{}, [], [a: 1]}

Why is that needed?

It’s because the Elixir AST is optimized for developer ergonomics when writing macros, it’s not a goal to be regular(in which case it would be more verbose).

Having them as 2-tuples, in combination with lists being literals in the AST, is what makes it easier to write macros for keyword blocks, like

defmacro schema(table, do: fields) do ...

If the AST were regular, you’d need to do something like this

defmacro schema(table, [{:{}, _, [:do, fields]}]) do ...

And you can imagine how it would be more complicated for other macros. Processing the keyword syntax for Ecto queries would be much more verbose, for example.

5 Likes

Thanks. Very clear. Though I still wonder about maps.

FWIW, Elixir properly handles 2-element tuples in uniform AST notation, for the sake of convenience.

defmodule Tuple2 do
  defmacro t(list), do: {:{}, [], list}
end

require Tuple2
Tuple2.t([:a, :b])
#⇒ {:a, :b}
4 Likes