Learn Elixir with a Rubyist, episode VI


[Head | Tail] & List Comprehension

Another week another article, glad to be back to publish one article a week, this is the sixth episode and as promised we will talk about one of the most used features of Erlang, the [head | tail] implementation and list comprehensions. Now is also a great opportunity to let me know what you’re thinking about the series on twitter and check the latest episodes:

Lists & [Head | Tail]

We have already talked about lists in the past articles, mainly on the - Episode IV - Elixir Types, Data Structures and Underscore, by now you may have realized that list are one of the main types in Elixir (and also in Erlang), because of that, there is a bunch o optimizations and tools that were created to support it.

Having a good support for lists is a common trait of functional languages because its tools enable you to achieve recursion easier, even without specific loop functions, as we already discussed on the last Episode V (Concurrency, Processes and Recursion).

[head | tail] was already present on Erlang, it enables you to split a list by taking away its first element and putting the rest of the list into a new list. Let’s start by checking some examples of how [head | tail] works:

# Declaring a list is really simple and straightforward,
# it can contain anything, from integers to other lists.
list = [1, 2, 3]

# Let's start by simply using pattern matching with the
# the [head | tail] syntax.
[h | t] = list


# You can see the value of `h` is now the first element
# of the list we had
h
# 1

# And `t` is the other elements from the same list but now
# into a new one.
t
# [2, 3]

# You can also use the Erlang `hd/1` and `tl/1` functions
# both take a list as argument and return its head or tail

hd(list)
# 1

tl(list)
# [2, 3]

# You can also use the [head | tail] syntax to push elements
# into a list, here `:atom` is being assigned as the head of 
# a list, and our old list as the tail, therefore it will 
# result in a new list with `:atom` as its first element

new_list = [:atom | list]
# [:atom, 1, 2, 3]

It may no seem but the head and tail implementation enables you to do awesome stuff, we checked some examples on the last episode, and will have event better ones on the next, but before jumping into practical examples let’s check more about list and its capabilities.

Lists Comprehensions

List comprehension is an old concept that is also present on Erlang, it can be used to construct lists (normally based on other lists) in a very natural, easy way, like a mathematician is used to do. It’s another common trait of functional languages because it enables you to loop through lists, filtering and mapping its elements.

Elixir brings a syntactic sugar for comprehensions, the for syntax , you can do most of common tasks using it, let’s check some examples of how it works:

 # The `for` syntax is pretty simple, it can be divided 
# into three different parts: an Enumerable, in this case
# a list `[1, 2, 3, 4]`, the variable to which each 
# elements is being assigned to (`i`), and the operation that
# will be performed using each element (`2 * i`).

for i <- [1, 2, 3, 4], do: 2 * i
# [2, 4, 6, 8]
# 
# As you can see each element of the list is being assigned
# to `i` and then multipled by `2`, resulting into a new list
# `for` also enable you to have filters applied to
# your comprehensions, let's check bellow:

for i <- [1, 2, 3, 4, 5], i > 2, do: 2 * i
# [6, 8, 10]

# As you can see, becasue we are now passing another argument,
# a filter (`i > 2`), elements that do now pass throught it
# won't be processes and therefore won't be on the new list.
# `1` and `2` were ignored and the numbers bigger than `2`
# were doubled as expected.

Lists Comprehensions + Pattern Matching

You can also put for together with pattern matching, by now you probably realized you can use pattern matching with almost anything on Elixir, by doing it we’ll be able you to filter the elements from the list (or other Enumerable).

This can be really useful when working with tuples (we talked about it on - Episode IV - Elixir Types, Data Structures and Underscore and multiple comprehensions. Let’s check how it works when using a list of tuples and pattern matching:

# We just create a list of tuples. In this case it 
# represents cities and its countries.

cities = [
  {"us", "new york"}, 
  {"us", "san francisco"}, 
  {"cn", "beijing"},
  {"fr", "paris"}
]

# Here we are using pattern matching by trying to match
# each tuple into the following pattern `{:us, city}`.
#
# The elements that doesn't match it will be ignored
# and the ones that do match will have its city name
# assigned to the `city` variable.

for {"us", city} <- cities, do: String.capitalize(city)
# ["New york", "San francisco"]

# As you can see only the first and second elements from
# our list were used and capitalized, because it was 
# the only ones that matched our pattern.

# Because we are using pattern matching we can do it 
# both ways, by also trying to match the city's name

for {country, "paris"} <- cities, do: (country)
# ["fr"]

Lists Comprehensions + into option

Another great and really useful feature that comes with the for implementation is the into option, it enables you to have the end result from the comprehension into another type that is not a list, like a map (we talked about maps on - Episode III - Maps, Functions + Pattern Matching = ❤️), here is a quick examples of how it works:

# Here we have a list with valid and invalids 
# responses, we want to get the error on that
# list and put it into a map, using the `error`
# key.
list = [
  {:ok, "valid"}, 
  {:ok, "valid"}, 
  {:error, "invalid argument"}
]


for {:error, error} <- list, into: %{}, do: {:error, error}
# %{error: ["invalid argument"]}
#
# by returning a tuple the `for` will understand 
# the the first element represents the key and the
# second one the value that should be on the map.
# And there is our Map, exactly how we wanted it.

I know it looks good enough already but gets even better, for also enables you to pass as many generators and filters as you need, check it out:

# This time we have 2 lists, one with integer 
# and another with atoms, lets use `for`
# on both at the same time
list_1 = [1, 2, 3]
list_2 = [:a, :b, :c]

for x <- list_1,
    y <- list_2 do 
      {x, y}
    end
# [
#  {1, :a}, {1, :b}, {1, :c}, 
#  {2, :a}, {2, :b}, {2, :c}, 
#  {3, :a}, {3, :b},{3, :c}
# ]
#
# The result will be the based on the comprehension 
# of both lists, getting a element from the first
# and going through all elements of the second one.

# You can have as many list you need, this concept
# also enable you to use as many filters as you want
# and even assign variables.

# In this example we will have two list, and we want to
# mutiply its elements by each other and puts the 
# result into a new list, but only if the result of the
# operation is bigger than `4`

list_1 = [1, 2]
list_2 = [3, 4]

for x <- list_1,
    y <- list_2,
    z = x + y,
    z > 4 do 
      z
    end
# [5, 5, 6]
#
# This is it! This example performed the following
# operations:
#
# 1 + 3 = 4
# 1 + 4 = 5
# 2 + 3 = 5
# 2 + 4 = 6
#
# But the result from the first calulation got excluded
# from our final result because it didn't pass 
# the `> 4` filter we had.

What’s Next?

Yay! Episode VI of Elixir with a Rubyist! I was hoping to get into the Task module into this episode but I thought it would be better to wait for the next one taking into account we had so much to check on lists.

This is a series of short bar-like conversations around Elixir, it’s aimed to help developers that are trying to understand and wrap their heads around it.

I hope you have liked it so far, if you do, please let me know on the comments bellow and over twitter.

On the next episode we will finally get into the Task module and understand how exchange information between processes.