Learn With Me: Elixir - The List Module Part 2 (#42)

Today I'm going to finish covering the rest of the functions in the List module. I honestly thought the functions in the List module wouldn't be very interesting, but it turns out that they have been quite interesting to learn about. That makes me glad I decided to learn about them in detail.

List.last/1

The List.last/1 function returns the last element in a list or nil if the list is empty. That's all there is to it.

iex> List.last([4, 3, "Sam", "Hans"])
"Hans"
iex> List.last([4])
4
iex> List.last([])
nil

List.myers_difference/2

The List.myers_difference/2 calculates and returns the myer's difference between two lists. A myer's difference consists of a sequence of changes that would cause one list to match the other. You can think of it as a diff between two lists much in the same way that git can produce a diff between two files.

I was aware of what a myer's difference was prior to this in the context of comparing strings to each other, but I've never had an opportunity to use it. I imagine that it could be quite useful for capturing the deltas between two lists, particularly if the lists were large and the changes tended to be small.

Let's take this function out for a spin and see what it comes up with.

iex> List.myers_difference([3, 4, -4, 2, 10], [3, 5, -4, 2, 10])
[eq: [3], del: [4], ins: [5], eq: [-4, 2, 10]]

The result is a keyword list. It looks the the :eq keys describe data that does not need to be modified. The :ins keys describe an insertion operation and the :del keys describe a deletion operation. So it looks like we have to delete a 4 in index 1 and insert a 5 in order for the first list to be the same as the second. The rest of the elements are unchanged. It looks like the instructions are meant to be run as an iterator moves from left to right.

Let's look at some more differences.

iex> List.myers_difference([1, 2, 3, 4], [4])
[del: [1, 2, 3], eq: [4]]
iex> List.myers_difference([1, 2, 3, 4], [1, 5, 6, 4])
[eq: [1], del: [2, 3], ins: [5, 6], eq: [4]]
iex> List.myers_difference([1, 2, 3, 4], [123, 8, 5, 2])
[del: [1], ins: [123, 8, 5], eq: [2], del: [3, 4]]

That last one is particularly interesting to me. Even though the two lists share a 2, they are in completely different places. The myer's difference preserves that 2, inserting numbers before it and deleting the numbers after it. It's very much like a git file diff if each item was a line with "ins:" being replaced with "+" and "del:" being replaced with "-".

I just went and looked up what algorithm git uses for diffing files, and it turns out that git does use the myer's difference algorithm by default. So that's why this difference output looks so familiar. Interesting. I love making connections like this: it helps me understand things better.

List.pop_at/3

The List.pop_at/3 function removes the value from a particular index in a list and returns it in a tuple along with the modified list.

I'm not sure why they called it "pop_at" rather than "remove_at". I guess it's because it also returns the value as well as removing it, making this a little different than a simple remove operation. The word "pop" made me think at first that it returned and removed the top item like when popping a value off of a stack, but I guess that since this isn't a stack data structure, a value can be popped from anywhere in the list.

As with many other functions, a negative index indicates an offset from the end of the list.

There is also a third optional parameter which indicates the value to return when the index doesn't exist in the list. This is nil by default, but can be overridden with something else.

Let's take a look at some examples.

iex> List.pop_at([1, 2, 3, 4, 5], 0)
{1, [2, 3, 4, 5]}
iex> List.pop_at([1, 2, 3, 4, 5], 2)
{3, [1, 2, 4, 5]}
iex> List.pop_at([1, 2, 3, 4, 5], -1)
{5, [1, 2, 3, 4]}
iex> List.pop_at([1, 2, 3, 4, 5], 10)
{nil, [1, 2, 3, 4, 5]}
iex> List.pop_at([1, 2, 3, 4, 5], 10, :not_found)
{:not_found, [1, 2, 3, 4, 5]}

List.replace_at/3

The List.replace_at/3 function removes the value at the specified index and replaces it with another. A negative index indicates an offset from the end of the list.

iex> List.replace_at([1, 2, 3, 4, 5], 0, "one")
["one", 2, 3, 4, 5]
iex> List.replace_at([1, 2, 3, 4, 5], 2, "three")
[1, 2, "three", 4, 5]
iex> List.replace_at([1, 2, 3, 4, 5], -1, "five")
[1, 2, 3, 4, "five"]
iex> List.replace_at([1, 2, 3, 4, 5], 6, "six")
[1, 2, 3, 4, 5]

It seems that if the index does not exist in the list, the original list is returned.

List.starts_with?/2

The List.starts_with?/2 function indicates whether a list starts with a prefix that consists of a sequence of elements. The prefix being tested for must also be a list. If an empty list is passed as a prefix, the function always return true.

iex> List.starts_with?([1, 2, 3, 4, 5], [1, 2])
true
iex> List.starts_with?([1, 2, 3, 4, 5], [3, 4])
false
iex> List.starts_with?([1, 2, 3, 4, 5], [1, 2, 3])
true
iex> List.starts_with?([1, 2, 3, 4, 5], [1])
true
iex> List.starts_with?([1, 2, 3, 4, 5], [])
true
iex> List.starts_with?([1, 2, 3, 4, 5], ["one", "two"])
false

List.to_atom/1

The List.to_atom/1 function converts a character list to an atom. The documentation warns us that this function does not support code lists that contain code points that are greater than 0xFF. So the character list can contain mostly basic ASCII characters, with 128 additional code points beyond the basic ASCII range.

iex> List.to_atom('ok')
:ok
iex> List.to_atom('bob')
:bob
iex> List.to_atom('This is a full sentence atom.')
:"This is a full sentence atom."
iex> List.to_atom([0, 1, 2, 3, 4])
:"\0\x01\x02\x03\x04"
iex> List.to_atom([250, 251, 252])
:├║├╗├╝
iex> List.to_atom([0xFD, 0xFE, 0xFF, 0x100, 0x101])
:├╜├╛├┐─Ç─ü
iex> List.to_atom([455, 400, 350, 200])
:LJƐŞÈ

It looks like the function attempts to create an atom from anything that's thrown at it, even it it's outside the supported range. I'm guessing that it creates the atom byte by byte (0xFF being the max value for a byte) regardless of what those bytes contain.

I've noticed that the values displayed for the atom aren't shown correctly outside the basic ASCII code point range. I suspect that this is due to the IEx display limitations rather than the List.to_atom/1 function. So I'm going to assume that the code points in the range 128 - 255 (0x80 - 0xFF) are converted correctly, but are just not displayed correctly by IEx.

List.to_existing_atom/1

The List.to_existing_atom/1 function works like List.to_atom/1 except that the conversion will only succeed if the atom has already been created and placed in the atom allocation table in the Erlang runtime. If the atom does not already exist, an ArgumentError will be thrown.

iex> List.to_existing_atom('ok')
:ok
iex> List.to_existing_atom('bob')
** (ArgumentError) argument error
    :erlang.list_to_existing_atom('bob')
iex> :bob
:bob
iex> List.to_existing_atom('bob')
:bob

The :ok atom already exists because it's widely used by code in the Elixir standard library, but the bob cannot be converted to an atom until I explicitly create the :bob atom, causing it to be allocated in the global atom table.

List.to_float/1

The List.to_float/1 function converts a character list representation of a float to a float.

iex> List.to_float('3.14')
3.14
iex> List.to_float('1.0')
1.0
iex> List.to_float('This is not a float')
** (ArgumentError) argument error
    :erlang.list_to_float('This is not a float')

List.to_integer/1

The List.to_integer/1 function converts a character list representation of an integer to an integer.

iex> List.to_integer('5')
5
iex> List.to_integer('3423')
3423
iex> List.to_integer('3.14')
** (ArgumentError) argument error
    :erlang.list_to_integer('3.14')

List.to_integer/2

The List.to_integer/2 function converts a character list representation of an integer to an integer with a parameter indicating the base. So whereas List.to_integer/1 could only convert base 10 numbers, this function can convert base 2, 8, 16, or whatever other weird base you want to convert.

iex> List.to_integer('100', 10)
100
iex> List.to_integer('100', 2)
4
iex> List.to_integer('100', 8)
64
iex> List.to_integer('100', 16)
256
iex> List.to_integer('0xFF', 16)
** (ArgumentError) argument error
    :erlang.list_to_integer('0xFF', 16)
iex> List.to_integer('FF', 16)
255

It appears that numbers cannot have any sort of prefix like number literals can. So 'FF' can be converted, but '0xFF' cannot.

List.to_string/1

The List.to_string/1 function converts a character list to a string.

iex> List.to_string('Bob')
"Bob"
iex> List.to_string('This is a sentence.')
"This is a sentence."
iex> List.to_string([0, 1, 2, 3])
<<0, 1, 2, 3>>
iex> List.to_string([0xFF, 0x100, 0x101])
"├┐─Ç─ü"
iex> List.to_string([0xFF, 0x100, 0x101, 0x00])
<<195, 191, 196, 128, 196, 129, 0>>

It looks like this function is a strict code point to UTF-8 code unit converter. The list [0, 1, 2, 3] does not convert to a printable string, but the conversion is done anyway. The result is a binary that contains UTF-8 code units, just like any other string, but it's displayed as a binary literal because it contains code points that aren't printable.

The last example shows how single code points are converted into multiple UTF-8 code units when the code points are encoded. The code point 0xFF (255), for example gets converted into two single-byte code units when it's encoded as UTF-8, 195 and 191. See my earlier digression in Lwm 8 discussing code points and code units if you haven't read that already.

List.to_tuple/1

The List.to_tuple/1 function converts a list to a tuple. It just copies the elements in a list into a tuple and returns it.

iex> List.to_tuple([1, 2, "Sam", {:ok, 3.4}, "Shark"])
{1, 2, "Sam", {:ok, 3.4}, "Shark"}
iex> List.to_tuple([1, 2])
{1, 2}
iex> List.to_tuple([1])
{1}
iex> List.to_tuple([])
{}

List.update_at/3

The List.update_at/3 is similar to List.replace_at/3 except that instead of taking a value to use for updating, it takes a function that it uses to update. So you can tell it to apply the function to the value at index X and replace that value with the result of the function.

Here are some examples.

#Square the number at index 2
iex> List.update_at([1, 2, 3, 4, 5], 2, &(&1 * &1))
[1, 2, 9, 4, 5]
#Subtract one from the number at the last index
iex> List.update_at([1, 2, 3, 4, 5], -1, &(&1 - 1))
[1, 2, 3, 4, 4]
#Attempt to update a non-existent index
iex> List.update_at([1, 2, 3, 4, 5], 8, &(&1 + 1))
[1, 2, 3, 4, 5]

When I attempted to update an index that did not exist, the function returned the original list.

List.wrap/1

The List.wrap/1 function is an interesting one. It wraps the parameter in a list unless it's already a list. If the parameter is nil, an empty list is returned.

iex> List.wrap("Bob")
["Bob"]
iex> List.wrap({:error, 4})
[error: 4]
iex> List.wrap([1, 2, 3])
[1, 2, 3]
iex> List.wrap(nil)
[]

List.zip/1

The List.zip/1 function works a lot like Enum.zip/1, but with lists instead of enumerables. It zips together the elements from the same index in each list into a tuple, stopping when there are no more values in at least one of the lists.

I'm not sure why they also provided a version specific to lists if there was already one in the Enum module. My guess is that this version is more efficient when working with lists.

iex> List.zip([[1, 2, 3, 4, 5], ["a", "b", "c", "d", "e"], ["Fillmore", "Taft", "Nixon", "Van Buren", "Arthur"]])
[
  {1, "a", "Fillmore"},
  {2, "b", "Taft"},
  {3, "c", "Nixon"},
  {4, "d", "Van Buren"},
  {5, "e", "Arthur"}
]
iex> List.zip([[1, 2, 3, 4, 5], [1, 2], [6, 7, 8]])
[{1, 1, 6}, {2, 2, 7}]
iex> List.zip([[1, 2, 3]])
[{1}, {2}, {3}]
iex> List.zip([])
[]

When only one list was provided, the function still did the zipping, but since there were no other lists to zip, it just put each element in its own tuple.

That's it for the functions in the List module. That dive into the List module proved to be more interesting that I expected. I now understand a lot better the list functions available to me and how to use them.