Erlang from the ground up

Tuples: fixed groups of values

A tuple groups a fixed number of values under one term, and is matched by position. Tagged tuples like {ok, Value} are how Erlang functions hand back an answer and its reason together.

Part 7 of 12

Intermediate15 min readBack to Courses

A tuple is a fixed group

A tuple is written in braces: {point, 3, 4}. It holds a fixed number of values, and the count never changes. Where a list is a row of things of any length, a tuple is a record of a known size, and the difference decides which one to use.

The elements are found by position, counting from 1. element(1, {point, 3, 4}) is the atom point, element(2, T) is 3, and tuple_size(T) is 3. The first element of a tuple is usually an atom naming what the tuple is, which makes it a tag rather than a value.

Positions and counts are fixed, so reaching any element costs the same. Nothing about a tuple is ordered for walking, which is why lists and tuples are not interchangeable.

Tagged tuples are answers

{ok, 7} and {error, disk_full} are the ordinary way an Erlang function says how it went. The tag is the first element, so a case on it picks the branch, and the second element carries the result or the reason. Two outcomes, one shape, no exceptions needed.

{point, 3, 4} follows the same idea without a function: the tag says how to read the numbers after it. A function head that matches {point, X, Y} is both a check that the value is a point and a way to take the two coordinates out in one step.

split({7, "hi"}) at the end of this chapter returns {"hi", 7}: the same values, in the other positions. Nothing was changed, because nothing in Erlang is ever changed.

When a tuple is the right choice

Use a tuple when the meaning of each position is fixed and known: a point has an x and a y, an answer has a status and a value. Use a list when the count varies, and a map when the fields need names. Most of the tagged answers you will see in Erlang code are two- or three-element tuples.

Size is also a limit on shape. A tuple with more than a handful of positions is hard to read at the call site, and every caller has to agree on what position four means. Once a value crosses a module boundary and grows fields, a map with named keys is easier to change: adding an entry does not renumber the ones already there.

Reading a tuple by position, and by pattern

-module(prog).
-export([main/1]).

main(_) ->
    P = {point, 3, 4},
    io:format("size ~p~n", [tuple_size(P)]),
    io:format("tag ~p~n", [element(1, P)]),
    io:format("x ~p~n", [x_of(P)]),
    io:format("rect ~p~n", [area({rect, 3, 4})]),
    io:format("circle ~p~n", [area({circle, 3})]).

x_of({point, X, _Y}) -> X.

area({rect, W, H}) -> W * H;
area({circle, R}) -> 3 * R * R.

Groups of values, taken apart

Run it and check each position against the code. Then add a `{rect, 2, 5}` call and a tuple of your own with three elements.

Output
Click "Run" to execute your code

Tag every tuple that leaves a function. {ok, Value} and {error, Why} are readable at the call site, and the tag gives the caller a pattern to match on instead of a guess about what position two means.

Tuples are fixed, so changing one makes another

A tuple’s size is part of its value, so there is no way to add a fourth element to a three-element tuple. setelement(2, T, New) gives back a new tuple with that position replaced; the old tuple still has the old value. The same rule as everywhere else in Erlang applies: nothing is edited in place.

That fixed size is what makes a tuple cheap to read. The runtime knows exactly how many elements it holds, so element/2 and tuple_size/1 are constant-time operations. A list has to be walked to find its length, which is one reason the two shapes are not interchangeable.

The first element is usually a tag

A tuple such as {point, 3, 4} or {error, disk_full} uses its first element to say what the rest means. The tag turns a row of anonymous values into a small, readable record, and it gives function clauses something to dispatch on: one clause for {rect, W, H}, another for {circle, R}.

Tags also make a mistake visible. A function expecting {point, X, Y} given {rect, 3, 4} fails at the head, before it can compute a nonsense answer. That early failure is the point of matching on the tag rather than reading positions and hoping.

Three tuple mistakes show up again and again. Positions start at 1, so element(0, T) fails with badarg. A tuple is not a list: {1, 2} and [1, 2] are different values, and length does not work on the first. And a tuple pattern must give the exact size unless it uses no size at all, so {point, X} will not match a three-element point.

Try these next

  • Add a {tri, A, B, C} shape with its own area clause and call it once.
  • Replace position 2 of a point with setelement and print both tuples.
  • Write a function that takes {ok, Value} or {error, Reason} and prints a different line for each.

Read stock by tag

Run it and check each stock line against its tuple. Then add an item with a count between 1 and 4 and a fourth report shape of your own.

Output
Click "Run" to execute your code

Check what you learned

One question at a time. A wrong answer explains itself — take another go.

Part 1 of 5

What is element(2, {point, 3, 4})?