Learn With Me: Elixir - The Tuple Module (#26)
This is the first in a series of in-depth looks at various standard modules in Elixir. These are intended as a companion to the very good Elixir documentation, and not a replacement for that documentation. I'll be mixing these in with other content, so it doesn't get as boring.
In some cases, I may not be able to add much to what's already in the documentation when a function is really straightforward, but I'm hoping my explanations will help you learn what Elixir modules can do for you and what you would use each function for. It's not always obvious what you should use a function for or when you should use it, so these explanations will definitely help when you encounter such a situation.
Today I'm focusing on the Tuple module, which is where the functions are located that operate on tuples. As the Elixir documentation warns us, not every tuple-related function is in the Tuple module. Some are located in the Kernel module.
These tuple-related functions are located in the Kernel module.
elem/2
- retrieves an element in a tuple by indexput_elem/3
- updates a value in a tuple by indextuple_size/
- returns the number of elements in a tuple
I have no idea why these three tuple functions are in the Kernel module and not the Tuple module. To me, it would make more sense if they were in the Tuple module. I'm sure the Elixir designers had their reasons for doing so, but I don't know what those are.
The Tuple module documentation warns us that modifying an existing tuple is an O(n) operation, since it causes the entire tuple to be copied. This is very inefficient. As a result, the documentation warns us to think very carefully before using any tuple modification functions, since it implies we may be using a tuple as a dynamic collection rather than the static grouping of data it's supposed to be.
Tuple.append/2
Tuple.append/2
appends an element at the end of a tuple. This is an inefficient operation (O(n)), since it involves copying the entire tuple.
iex> Tuple.append({:ok, "Bob", 4}, ["a", "b", "c"])
{:ok, "Bob", 4, ["a", "b", "c"]}
iex> Tuple.append({2}, "two")
{2, "two"}
iex> Tuple.append({1, 2, 3}, 4)
{1, 2, 3, 4}
Tuple.delete_at/2
Tuple.delete_at/2
deletes the element at a particular index in the tuple. If the index does not exist, an error is raised.
This is an inefficient operation (O(n)), since it involves copying the entire tuple.
iex> data_tuple = {"Bob", :ok, 6}
{"Bob", :ok, 6}
iex> Tuple.delete_at(data_tuple, 2)
{"Bob", :ok}
iex> Tuple.delete_at(data_tuple, 1)
{"Bob", 6}
iex> Tuple.delete_at(data_tuple, 0)
{:ok, 6}
iex> Tuple.delete_at(data_tuple, -1)
** (ArgumentError) argument error
:erlang.delete_element(0, {"Bob", :ok, 6})
iex> Tuple.delete_at(data_tuple, 3)
** (ArgumentError) argument error
:erlang.delete_element(4, {"Bob", :ok, 6})
Tuple.duplicate/2
Tuple.duplicate/2
creates a new tuple and initializes it with a single data item that's repeated in every index in the tuple. So Tuple.duplicate("Doom", 8)
is the equivalent of {"Doom", "Doom", "Doom", "Doom", "Doom", "Doom", "Doom", "Doom"}
. This function just provides an easy way to create a repetitive tuple, and is just as efficient as creating the tuple using the literal.
Keep in mind that since data in Elixir is immutable and it only keeps once instance of data around, the tuple won't contain 8 copies of the string "Doom", but 8 references to a single string, "Doom". This makes a difference when the size of the data is large.
iex> Tuple.duplicate(10, 1)
{10}
iex> Tuple.duplicate(10, 0)
{}
iex> Tuple.duplicate(10, -1)
** (ArgumentError) argument error
:erlang.make_tuple(-1, 10)
iex> Tuple.duplicate(10, 3)
{10, 10, 10}
iex> Tuple.duplicate("Doom", 8)
{"Doom", "Doom", "Doom", "Doom", "Doom", "Doom", "Doom", "Doom"}
iex> Tuple.duplicate("gremlin", 30)
{"gremlin", "gremlin", "gremlin", "gremlin", "gremlin", "gremlin", "gremlin",
"gremlin", "gremlin", "gremlin", "gremlin", "gremlin", "gremlin", "gremlin",
"gremlin", "gremlin", "gremlin", "gremlin", "gremlin", "gremlin", "gremlin",
"gremlin", "gremlin", "gremlin", "gremlin", "gremlin", "gremlin", "gremlin",
"gremlin", "gremlin"}
Tuple.insert_at/3
Tuple.insert_at/3
inserts a value at a particular index in a tuple. This is an inefficient operation (O(n)), since it involves copying the entire tuple. Use sparingly.
iex> data_tuple = {"Bob", :ok, 6}
{"Bob", :ok, 6}
iex> Tuple.insert_at(data_tuple, 0, "Zim")
{"Zim", "Bob", :ok, 6}
iex> Tuple.insert_at(data_tuple, 1, "Zim")
{"Bob", "Zim", :ok, 6}
iex> Tuple.insert_at(data_tuple, 2, "Zim")
{"Bob", :ok, "Zim", 6}
iex> Tuple.insert_at(data_tuple, 3, "Zim")
{"Bob", :ok, 6, "Zim"}
iex> Tuple.insert_at(data_tuple, 4, "Zim")
** (ArgumentError) argument error
:erlang.insert_element(5, {"Bob", :ok, 6}, "Zim")
Inserting a value at an index just past the end of the tuple is the equivalent of appending it (and probably is slightly less efficient at doing so). That behavior surprised me: I was expecting an error. However, attempting to insert any values past that will result in an error.
Tuple.to_list/1
Tuple.to_list/1
copies the contents of a tuple into a list, which is more friendly for sequential access and certain types of modifications. I could see this being useful.
iex> Tuple.to_list({"Zim", "Dib", "Gaz"})
["Zim", "Dib", "Gaz"]
iex> Tuple.to_list({:ok, [1, 2, 3]})
[:ok, [1, 2, 3]]
iex> Tuple.to_list({3})
[3]
iex> Tuple.to_list({})
[]