4. Tuples and atoms

The atom and tuple are two simple data structures at the core of much of Elixir.

An atom is a constant whose value is its own name. :ok is an atom, its value is :ok. You can think of an atom as a global symbol that anyone can use and reference. If you had a function that classified words, it may want to return :noun if it classifies the word passed in as a noun, for example.

Tuples are a way of grouping together values, and are represented with curly brackets. {:hello, :world} is a tuple that contains the atoms :hello and :world. Tuples can hold any type of value - {1, :e, 3, "t"} is also a valid tuple.

In this chapter, we will be going over both of these core data types. We will also be introducing a new tool to our testing toolkit, to aid us in our never-ending exploration of the Elixir language and its libraries.

Let’s go!

4.1. Interactive Elixir

Writing tests and running them is great tool to learn and verify our learnings. This is what we’ve been doing up to now: picking a problem to solve, writing tests that verify how it should be solved, then writing the implementation of the module that solves the problem.

This approach shines precisely when we know exactly what we want. However, there are times where the precise behavior we want is not known, and we need to explore a bit further before deciding on a path to take. Or when we want to quickly verify something without setting up a test to do so.

Luckily, Elixir has an interactive shell for precisely these sort of scenarios. It comes with your elixir installation, so you already have it ready to use. You can start the shell by running the iex command.

$ iex
Erlang/OTP 24 [erts-12.0.3] [source] [64-bit] [smp:16:16] [ds:16:16:10] [async-threads:1] [jit]

Interactive Elixir (1.12.2) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)>

You can type in any sort of Elixir code you want, and run it. IEX will evaluate the expression and print the result. For example, you can just type in a string, which will be evaluated and printed:

iex(1)> "hello world"
"hello world"
iex(2)>

The number after iex when in the shell increments everytime we run a command. I’ll omit it from my code snippets, to make it clear that it doesn’t need to match yours!

4.1.1. Atoms

Start the shell by running iex, if you have not already.

Let’s define an atom, :hello.

iex> :hello
:hello

As mentioned before, any two atoms with the same name have the same value. We can try that out, too:

iex> :hello == :hello
true

Atoms underlie many aspects of Elixir. In fact, true is actually just an atom. Elixir simply handles it slightly differently so you do not need to prefix it with :.

iex> :true === true
true
iex> is_atom(true)
true

To represent "nothing", there is also the special nil atom.

iex> :nil === nil
true
iex> is_atom(nil)
true

Atoms cannot begin with a number.

iex> :1hundred
** (SyntaxError) iex:8:1: unexpected token: ":" (column 1, code point U+003A)

We can quote the value of our atoms if we want. This is useful if our atom contains spaces.

iex> :"hello world"
:"hello world"

But it is discouraged to use quotes if they are not necessary.

iex> :"hello"
warning: found quoted atom "hello" but the quotes are not required. Atoms made exclusively of ASCII letters, numbers, underscores, and optionally ending with ! or ? do not require quotes

:hello

4.1.2. Tuples

Tuples are fix-sized containers for multiple elements. They are delimited with brackets.

iex> {1, :a, "foo", true}
{1, :a, "foo", true}

We can determine the size of a tuple with tuple_size/1.

iex> tuple_size({1, 2})
2

Tuples are often used in the return value of functions. In Elixir, returning an :ok or :error atom as the first element in a tuple is one way to indicate that the function failed or returned invalid input. The File.read/1 function, for example, uses this pattern.

iex> File.read("/this/file/does/not/exist.txt")
{:error, :eonent}
iex> File.read("/etc/hosts")
{:ok, "127.0.0.1 localhost\n"}

We can get an element within the tuple with the elem/2 function.

iex> elem({:foo, :bar}, 0)
:foo

And update it with put_elem/3.

iex> put_elem({:foo, :bar}, 0, :huh)
{:huh, :bar}

4.1.3. Match operator (=)

In Elixir, the equals sign = is called the match operator. It works similar to the matching mechanisms we have seen previously in case and function definitions, among other things.

In its simplest form, we can match the value on the right hand side into the left hand element.

iex> a = 4
4
iex> a
4

But the left hand side of the match operator need not be a variable. We can use another value too, though it might not match. If it doesn’t match, Elixir will raise a MatchError.

iex> 4 = 4
4
iex> 5 = 4
** (MatchError) no match of right hand side value: 4

The match operator works hand in hand with pattern matching. Elixir will always try to match the right hand parameter into the structure on the left hand side. We can use that with tuples to do some pretty cool things, like assigning variables to the value of elements in the tuple.

iex> {a, b} = {:foo, :bar}
{:foo, :bar}
iex> a
:foo
iex> b
:bar

We can also match on specific values. This is useful when using functions that use the :ok or :error tuple convention to report success or failure of the operation, like File.read/1. By matching specifically on the :ok value, we can make sure our code only succeeds and continues if the operation returns :ok like we presumably expect. Otherwise it will raise a MatchError.

iex> {:ok, content} = File.read("/etc/hosts")
{:ok, "127.0.0.1 localhost\n"}
iex> content
"127.0.0.1 localhost\n"
iex> {:ok, content} = File.read("/this/file/does/not/exist.txt")
** (MatchError) no match of right hand side value: {:error, :enoent}

We can match on the value of variables, instead of doing an assignment operation. This is done with the pin ^ operator.

iex> a = 4
4
iex> ^a = 4
4
iex> ^a = 5
** (MatchError) no match of right hand side value: 5

This pin operator is very useful in tuple pattern matching.

iex> desired = :ok
:ok
iex> {^desired, content} = File.read("/etc/hosts")
{:ok, "127.0.0.1 localhost\n"}

4.1.4. Comments on iex

In this section we have explored atoms, tuples, and the match operator through iex. It is a great tool, and you may wonder why we haven’t used it in previous sections, or why we don’t use it all the time. After all, you just type code in the console and click enter and see it run!

I found that when writing code and learning a language, it is best to make sure the manner in which I learn it is as similar as possible to the manner in which I will use the language in the future. IEX is great for exploring concepts, but you cannot use it to actually build up modules and their tests and so on, which is what you will be using Elixir to do in the near future!

When I first learned Elixir, most of the books I found had an IEX-heavy approach. When I finished those books, I understood how all of the little parts of Elixir work (like the match operator) but did not know how to actually use those to make useful modules. Because I always learned with IEX, I also was not familiar with the various compilation and runtime errors I later encountered when writing code and running tests. When I did encounter those, I did not know what to do.

IEX is just another tool in our Elixir toolbox. I use it all the time and encourage you to use it too! We will use it here and there to explore concepts that are best explained in a highly interactive iex shell. However, writing tests and modules will continue to be our main method of learning and exploring Elixir, as the title of this book indicates.

4.2. Utils.reverse_tuple/1

In programming, it is common to create a "utility" library that contains a hodgepodge of useful functions that could be useful in a variety of scenarios. Generally, these functions are things that could be in the standard library, but for one reason or another, are not.

Let’s create a utility module of our own, Utils, that will contain some useful utility functions for working with atoms and tuples.

Let’s start with a function that reverses a tuple. Imagine if we found a great library that does exactly what we want, but returns {value, :ok} instead of {:ok, value} as is standard convention. We could still use it without tripping over ourselves if only we could reverse it.

As always, start with the test:

Listing 1. tuples-and-atoms/utils_test.exs
Code.require_file("utils.ex")
ExUnit.start()

defmodule UtilsTest do
  use ExUnit.Case

  test "handles empty tuple" do
    assert Utils.reverse_tuple({}) == {}
  end

  test "handles single element tuple" do
    assert Utils.reverse_tuple({1}) == {1}
  end

  test "handles multiple element tuples" do
    assert Utils.reverse_tuple({1, 2}) == {2, 1}
    assert Utils.reverse_tuple({1, 2, 3}) == {3, 2, 1}
  end
end

Then, let’s create an empty or "stub" implementation so that the test can run to completion.

Listing 2. tuples-and-atoms/utils.ex
defmodule Utils do
  def reverse_tuple(tuple) do
    tuple
  end
end

And let’s run our tests to verify that our tests fail, as we would expect.

$ elixir utils_test.exs
..

  1) test handles multiple element tuples (UtilsTest)
     utils_test.exs:15
     Assertion with == failed
     code:  assert Utils.reverse_tuple({1, 2}) == {2, 1}
     left:  {1, 2}
     right: {2, 1}
     stacktrace:
       utils_test.exs:16: (test)

Finished in 0.09 seconds (0.08s on load, 0.00s async, 0.01s sync)
3 tests, 1 failure

Randomized with seed 639220

Our "trivial" test cases that handle the empty and single tuple cases pass fine, but our last one failed, as we would expect.

4.2.1. Finding a reversing algorithm

Now that we have our test, we can think about the implementation. Let’s walk through one of the examples in our test, {1, 2, 3}. To reverse that one, all we need to do is to swap the first and last values. As our test says, the reversed version is simply {3, 2, 1}.

Let’s try a more complicated case, of {1, 2, 3, 4, 5}. First we swap the first value, 1, with the last value, 5. After that, we swap the second value, 2, with the second-to-last value, 4. The middle value we don’t need to do anything with, since there is nothing to swap. We end up with {5, 4, 3, 2, 1}. We can represent the transformation of the tuple visually, using ^ to represent the current indexes we are considering at each step.

beginning:     {1, 2, 3, 4, 5}
                ^           ^
after swap #1: {5, 2, 3, 4, 1}
                   ^     ^
after swap #2: {5, 4, 3, 2, 1}
                      ^^

So our algorithm for reversing a tuple is:

  1. Set the left_index to 0, Set the right_index to the last possible index.

  2. If the left_index is less than the right_index, swap those two values. After swapping, increment the left_index and decrement the right_index and try again.

  3. Else, stop.

Let’s translate that into code. Recall from earlier that we can get the value of a tuple index with elem/2 and update a tuple value at a given index with put_elem/3. At the beginning of our reverse_tuple/1 function, we can compute the left_index and right_index, and continue the algorithm by calling another function that does the second and third steps.

Putting this together, we end up with a function that looks something like this:

Listing 3. tuples-and-atoms/utils.ex
defmodule Utils do
  def reverse_tuple(tuple) do
    left_index = 0
    right_index = tuple_size(tuple) - 1
    reverse_tuple(tuple, left_index, right_index)
  end

  def reverse_tuple(tuple, left_index, right_index) do
    if left_index < right_index do
      # get values to swap
      left_value = elem(tuple, left_index)
      right_value = elem(tuple, right_index)

      # swap the values
      tuple = put_elem(tuple, left_index, right_value)
      tuple = put_elem(tuple, right_index, left_value)

      # process the next indexes
      reverse_tuple(tuple, left_index + 1, right_index - 1)
    else
      tuple
    end
  end
end

And if we run the tests, all of them pass now:

$ elixir utils_test.exs
...

Finished in 0.09 seconds (0.08s on load, 0.00s async, 0.01s sync)
3 tests, 0 failures

Randomized with seed 765384

4.2.2. Private functions with defp

Great! We made this useful piece of code for others to use, all they need to do is add our Utils module to their code. However, we’ve gotten reports that others are not sure whether to use reverse_tuple/1 or reverse_tuple/3. After all, our module provides both! Worse yet, we’ve heard cases of other engineers using reverse_tuple/3 incorrectly. They accidentally swap the order of the two index arguments, and then nothing happens! We tell them to use the reverse_tuple/1 function instead, as it handles everything for them, but they don’t understand why we have the other reverse_tuple/3 one, then.

We only want to use reverse_tuple/3 ourselves, in short. We don’t want others to use it, because they don’t need to. Luckily, Elixir has a handy defp construct that defines a function, just like def, but makes that function only available to other functions within the same module. The "p" stands for private, and such functions are usually called "private functions".

Let’s update our reverse_tuple/3 function to be private:

Listing 4. tuple-and-atoms/utils.ex
defmodule Utils do
  def reverse_tuple(tuple) do
    left_index = 0
    right_index = tuple_size(tuple) - 1
    reverse_tuple(tuple, left_index, right_index)
  end

  defp reverse_tuple(tuple, left_index, right_index) do
    if left_index < right_index do
      # get values to swap
      left_value = elem(tuple, left_index)
      right_value = elem(tuple, right_index)

      # swap the values
      tuple = put_elem(tuple, left_index, right_value)
      tuple = put_elem(tuple, right_index, left_value)

      # process the next indexes
      reverse_tuple(tuple, left_index + 1, right_index - 1)
    else
      tuple
    end
  end
end

Run the tests once more to make sure everything is still running correctly.

One thing to note is that defp functions cannot be called from our testing code! This is because our test module is UtilsTest, while our module is Utils. In our case, we do not have any current tests that call reverse_tuple/3 directly. In fact, this "limitation" is a good thing. What we want to test is the publicly defined behavior of our module, since that is what others who use our module will use, in the end. Because we are only testing our public functions, we are free to refactor our implementation and private functions however we want. We could update reverse_tuple/3 to take a fourth parameter, for example, and both our tests and others who use our code would be unaffected.

4.3. Utils.atom_exists?/1 and Utils.create_existing_atom/1

Elixir runs on the BEAM, a virtual machine. The BEAM provides Elixir with a bunch of niceties, one of which is automatic garbage collection. When you create a tuple, for example, the BEAM will allocate memory for that tuple to live in. When that tuple is no longer used, the BEAM will delete it, freeing up that memory for other uses.

Atoms, like everything else, also use up memory. However, the BEAM will never garbage collect them. Atoms, in other words, take up memory until the currently running program terminates.

Elixir has some built-in functions for converting strings to atoms. However, because atoms are never garbage collected, this is potentially dangerous. If you create an atom for every word in the world, you will run out of memory and your computer may crash.

In this section, we will explore some of those built-in functions Elixir has for creating atoms, then create some of our own that are a bit safer.

4.3.1. IEx: String.to_atom/1 and String.to_existing_atom/1

Elixir has String.to_atom/1 for converting strings into atoms. Let’s load up IEx and try it out.

iex> String.to_atom("ok")
:ok
iex> String.to_atom("gobbledygook")
:gobbledygook

This function will always create an atom from the provided string. As mentioned, this is potentially dangerous. Elixir has another function, String.to_existing_atom/1 that will only create the atom if it already exists, as a safer alternative.

iex> String.to_existing_atom("ok")
:ok
iex> String.to_existing_atom("does_not_exist")
** (ArgumentError) errors were found at the given arguments:

  * 1st argument: invalid UTF8 encoding

    :erlang.binary_to_existing_atom("does_not_exist", :utf8)

It gives a pretty confusing error message when the atom does not exist, though. At least, it confuses me! What does "UTF8 encoding" have to do with whether an atom exists or not?!

Let’s create a more user-friendly function for checking if an atom exists.

4.3.2. Boolean-returning functions end with ?

The first function we want to create is atom_exists?/1. This function will receive a string and check if an atom corresponding to that string already exists. If it does, it will return true, otherwise it will return false.

By convention, in Elixir functions that return a boolean are suffixed with ?. Ours always returns a boolean, so we follow that convention.

Let’s start, as always, with a test:

Listing 5. tuples-and-atoms/utils_test.exs
include::tuples-and-atoms/3/utils_test.exs

Then create a minimal implementation so that we can run our tests to completion.

Listing 6. tuples-and-atoms/utils.ex
  def atom_exists?(string) do
    true
  end

Run our tests to verify they fail, as expected.

$ elixir utils_test.exs
warning: variable "string" is unused (if the variable is not meant to be used, prefix it with an underscore)
  utils.ex:25: Utils.atom_exists?/1

  1) test atom_exists?/1 (UtilsTest)
     utils_test.exs:20
     Assertion with == failed
     code:  assert Utils.atom_exists?("does_not_exist") == false
     left:  true
     right: false
     stacktrace:
       utils_test.exs:22: (test)

...

Finished in 0.08 seconds (0.07s on load, 0.00s async, 0.01s sync)
4 tests, 1 failure

Randomized with seed 559200

4.3.3. Rescuing ourselves from errors with rescue

Now let’s fill in the implementation that will make our tests pass. From our IEx session, we know that we can use String.to_existing_atom to check if an atom already exists. If the atom does exist, the function will return the atom, otherwise it will raise an ArgumentError.

So for the case where the atom does exist, we simply call the function and return true as long as the function succeeds. But if the atom does not exist, how can we rescue ourselves from the ArgumentError, so that our code can continue processing?

In Elixir, there is the rescue construct, which can be placed at the end of any function definition. Code there will execute to handle any errors we encountered during the execution of the code within the function body.

Listing 7. tuples-and-atoms/utils.ex
  def atom_exists?(string) do
    String.to_existing_atom(string)
    true
  rescue
   ArgumentError -> false
  end

rescue uses pattern matching too! As shown here, we match against the type of the error, only returning false if we get the expected ArgumentError. Otherwise, if an unexpected error occurs, like a MatchError, the error will "bubble up" since our raise block does not match it.

Run the tests once more to verify everything passes.

4.3.4. Representing status with :ok and :error

At the beginning of the chapter, we saw that some functions in Elixir’s standard library return a tuple, with either :ok or :error as the first element to indicate whether the operation was a success or failure. This is the preferred way in Elixir to handle errors.

Let’s create our own create_existing_atom/1, which will "wrap" String.create_existing_atom to conform to this pattern. Instead of raising an ArgumentError when the atom doesn’t exist, it will instead return {:error, "does not exist"}. And if the atom does exist, it will return {:ok, :the_atom}, where :the_atom is the atom representation of the string passed in.

First, let’s create the test:

Listing 8. tuples-and-atoms/utils_test.exs
  test "create_existing_atom/1" do
    assert Utils.create_existing_atom("ok") == {:ok, :ok}
    assert Utils.create_existing_atom("nowhere_to_be_found") == {:error, "atom does not exist"}
  end

Then create a stub of the function, so we can run our tests:

Listing 9. tuples-and-atoms/utils.ex
  def create_existing_atom(string) do
    {:ok, :ok}
  end

And now we can run our tests.

$ elixir utils_test.exs
warning: variable "string" is unused (if the variable is not meant to be used, prefix it with an underscore)
  utils.ex:32: Utils.create_existing_atom/1

....

  1) test create_existing_atom/1 (UtilsTest)
     utils_test.exs:25
     Assertion with == failed
     code:  assert Utils.create_existing_atom("nowhere_to_be_found") == {:error, "atom does not exist"}
     left:  {:ok, :ok}
     right: {:error, "atom does not exist"}
     stacktrace:
       utils_test.exs:27: (test)

Finished in 0.08 seconds (0.07s on load, 0.00s async, 0.01s sync)
5 tests, 1 failure

Randomized with seed 295258

Now we can create our implementation to make the tests pass. It will look pretty similar to our atom_exists? function, but instead of returning a boolean, it will return our result tuple instead.

  def create_existing_atom(string) do
    atom = String.to_existing_atom(string)
    {:ok, atom}
  rescue
    ArgumentError -> {:error, "atom does not exist"}
  end

And just like that we have created a new interface for creating atoms from strings. Don’t forget to run the tests to make sure everything is passing.

Our Utils and our UtilsTest modules are a bit larger than the modules we have created before. When reading over our UtilsTest module, it is not immediately clear which tests correspond to what function. In fact, our first three tests all correspond to the reverse_tuple/1 function, but do not say that in their test description.

When writing tests in Elixir with ExUnit, we have a tool for grouping together related tests, the describe/2 macro.

It works pretty simply. All we need to do is wrap the various test/2 definitions to be within our describe/2 block:

include::tuples-and-atoms/5/utils_test.exs

Since we refactored our tests, run them again to make sure they are still passing.

One other thing the describe/2 macro does is make the failing test output include the describe/2 text before the test/2 text for the test title. So if our "handles empty tuple" test fails, for example, the title of the test in the output will be "reverse_tuple/3 handles empty tuple".

4.4. Conclusion

With knowledge about tuples, atoms, and the match operator under our belt, we are well on our way to becoming proficient in Elixir! We also got to learn about some other useful things, like rescue and the result tuple pattern. Onward!

4.4.1. What we learned in this chapter

  • Tuples group together a fixed set of values.

  • Atoms are constants whose value are their own name.

  • true and false are also atoms.

  • The match operator, =, can be used to bind values to a variable. It can use pattern matching and even be used outside of typical variable assignment.

  • The pin operator, ^, can be used to force a match to use the value of a variable.

  • IEx is Elixir’s interactive shell. It is great for exploring the language and trying things out.

  • defp defines private functions that cannot be used outside the module.

  • Using defp lowers the "surface area" of your module, making it easier to use and test.

  • Elixir runs on the BEAM, which uses garbage collection to clean up unused memory.

  • Atoms are never garbage collected.

  • Functions that return a boolean usually end with a question mark, ?.

  • We can rescue ourselves from errors with rescue at the end of a function block.

  • Result tuples that contain :ok or :error as the first element are a great way to return whether the function succeeded or not.

  • We can group related tests together with describe.

4.4.2. Exercises

  • Instead of using if and else, update our reverse_tuple/2 function to leverage function definition guards instead. You will end up with two reverse_tuple/2 definitions, one corresponding to the current if branch, and the other corresponding to the current else branch.

  • Update reverse_tuple/1 to only accept tuple parameters, and add a test for it. You can use is_tuple/1 to check if something is a tuple.

  • We went over a five-element tuple example for reverse_tuple/1 when planning out the implementation. However, we forgot to add that useful example to our test suite! Add a new test for reverse_tuple/1 for the five-element tuple size case. Also add any other cases you think would be useful, like an even-size tuple size case, for example.

  • Open up IEx and use our Utils module directly. You can load the code into your session by using Code.require_file("utils.ex"). Try calling reverse_tuple/1 with different values. Verify that it is not possible to call reverse_tuple/3 directly.