Why do tuples exist?

Hi,

I have recently started devoting some more time to learning Elixir and a question that always nags me is why do tuples exist in Elixir? Is it purely because they are a side effect of erlang and therefore had to be brought across?

Whether to use a tuple or a list feels like a bit of a grey area to me.

Where do people draw the line when it comes to deciding on whether to use a tuple or a list?

Thanks,
Nate

5 Likes

Older functional language such as Lisp do not have tuples. So they are not strictly speaking necessary. However, they make certain ideas clearer, certain operations cheaper, so most modern languages, functional or not, tend to have it.

As for where to draw the line, just look at elixir’s core library. Ask yourself why a tuple is used instead of a list anywhere you see it. Eventually you will have the idea.

3 Likes

Tuples and Lists are both compound data structures. Lists typically store multiple instances of the same type of thing. Tuples typically store different types of things. Tuples are lousy at enumerating through the elements. Lists are great at that.

3 Likes

Ok, thanks.

I guess it is going to come down to how the community has decided that tuples should be used? For example in function responses and representing things like x,y coordinates. That is where I have seen them.

To me, it seems that if Tuples are not great at enumerating, then Lists have them covered.

Tuples have an advantage over lists because they’re stored contiguously. They have better ‘spatial locality’. Caching benefits from two types of locality, ‘temporal’ which is reusing a specific part of memory, or ‘spatial’ locality, which is the ability to load a segment of data into a CPU cache / RAM and have fast access to parts of that data because they’re close together. Tuples help with the later. Lists potentially require more random memory access.

Due to these characteristics, tuples are comparatively fast to read and create but slow to modify. More detailed information can be found here: Efficient Erlang - Performance and memory efficiency of your data by …

11 Likes

Thanks!

1 Like

Tuples are like immutable arrays in other languages.

Additionally, they are used as a workaround to overcome the problem of functions not being able to return more than one value. With tuples you can encapsulate an arbitrary amount of return values.

3 Likes

I would say the main benefit is access time. Tuple are fast for reading but not for adding elements.
If the size of your data structure is not going to change, tuple is a good option. You can determine the size of a time in constant time, while with a list you need to traverse the whole list to find out how long it is.

6 Likes

To pile in on this one :slight_smile:

Tuples are: strictly positional (you won’t be moving their contents around, nor will you be referencing the contents by keys, and pattern matches must be exact), are not Enumerable (so you won’t get to use the Enum module), relatively space efficient (caveats apply) and fast for random access (due to positional nature). Various mutations are provided in the Tuple module.

Maps are: fast lookups, good update performance, values have lookup keys, they are Enumerable, and pattern matches are quite free-form due to the key’d values

Lists are: Enumerable, suited to collections of items, poor for random access at large sizes, but good for iterating and modifying. The swiss-army-knife data bag of Elixir, and often more than fast-enough in most cases.

Keyword lists are: Lists of 2-Tuples!

As Keyword lists show, these can all be mixed and matched: tuples as the values (or keys!) of maps, maps as the values in a list, lists as elements in tuples … algebraic types ftw!

10 Likes

This blog from @sasajuric is also a great resource. Comparing some different data structures in different scenarios. Seems like tuples definitely have their place in some use cases

https://www.theerlangelist.com/article/sequences

7 Likes

Indeed, and not only for performance reasons. When one wants to ensure that the data is of a specific size, they are perfect.

Lists and maps can be of different sizes, though they can be checked for length (at a cost), while with a tuple ensuring an exact count of elements can be done with a simple pattern match.

Keyword is a neat example of this. As a list of tagged tuples, it guarantees that each entry has exactly 2 members, and that the entries appear in the order of creation (thanks to being in a list). This would be more awkward with either lists-only or a map, and given how Keyword lists tend to be used would likely be slower (well, I haven’t benchmarked that, but it wouldn’t surprise me in the least)

The common {:ok, data} | {:error, reason} pattern for function returns is another.

4 Likes

Excellent comments! Thank you

I think this one would be a typical beginners mistake (or maybe it was just me), which is to try to pattern match on keyword lists, specially options passed to a function.

3 Likes

As you point out, tuples are immutable. The reasons for having immutable types apply to tuples:

  • copy efficiency: rather than copying an immutable object, you can alias it (bind a variable to a reference)
  • comparison efficiency: when you’re using copy-by-reference, you can compare two variables by comparing location, rather than content
  • interning: you need to store at most one copy of any immutable value
  • there’s no need to synchronize access to immutable objects in concurrent code
  • const correctness: some values shouldn’t be allowed to change. This (to me) is the main reason for immutable types.

Note that a particular Python implementation may not make use of all of the above features.

1 Like

Tuples are amazing to help your code grow while still maintaining order in the chaos. I have found over the years that having almost all your (especially business) functions returning either {:ok, some_object} or {:error, some_error_reason} helps keep code consistent and easy to understand.

And when you pair it with the with keyword, which allows you to specify multiple statements/operations in series and returns the inside of the block when everything succeeds but returns the non-matching result when one of the operations fails is amazing behavior that keeps everything very predictable.

Like with most software development, YMMV, but overall i love tuples as function returns :slight_smile:

2 Likes

Here’s what i mean:

with {:ok, object_1} <- operation_1(),
     {:ok, object_2} <- operation_2(object_1) do
  final_operation(object_2)
end

If all pattern matching goes ok, the result of final_operation/1 is returned. If any of the others fail, you get the certainty that with will return {:error, some_error} as long as all functions have the same type of return.

2 Likes

While they look a lot like them, tuples are not an alternative to lists, they are an alternative to structs.

The usecase for tuples and structs is to group together several values that belong together, e.g. because they’re different attributes of the same thing. You know in advance how many attributes there are, and what they mean.
A list is for a collection of things that are the same ‘shape’. Usually they’re the same type - i.e. all integers, or all the same type of struct, or all functions. If they’re different types then they’re still expected to be handled in the same way. If you have “some number of things that are all similar but I don’t know how many in advance” then you want a list, e.g. if you’re reading in a file and you separate it into each line, then you would use a list (of strings).

To go back to tuples and structs, the classic ‘person’ example in so many code examples is good here. For our purposes a person has two attributes - name (a string) and age (a number). Here is how to return each of the options from a function:

As a tuple:

def func() do
  {"John", 33}
end

As a struct:

defmodule Person do
  defstruct [:name, :age]
end
def func() do
  %Person{name: "John", age: 33}
end

The tuple is easiest: you don’t have to declare anything in advance you just create it. Users of this function then have to know (e.g. through documentation or convention) that the first element is the name, and the second element is the age. They also get unwieldy when you’ve got more than a few elements - you end up with difficult to read code accessing elements. Adding an extra parameter also usually required touching a lot of code Tuples are excellent for pairing up 2 or 3 things together, and passing them around locally (e.g. internal to a module), or where there is a good convention (e.g. the {:error, message} return convention).

For anything more complicated than a couple of elements you want a struct - rather than having to know which position is which member the fields are named. Structs enforce that you haven’t forgotten any of the members, allow you to add new members without affecting most code and provide more information to autocomplete and type checking (also pattern matching if you want). Structs are a great choice a lot of the time.

You could also use a map for this purpose, but you usually don’t want to. A map is closer to a list in that it’s a collection of an unknown number of items, but rather than looking them up by position in an array, you look them up by name. Structs are actually implemented ‘under the hood’ as maps, but to use them in the way I’m talking about above you want the guarantees that structs provide. For example, when using a struct you can guarantee that a given field in the struct is present (because Elixir ensures that), whereas with a map you would always have to check in case you were passed a malformed one.

2 Likes