Learning a new programming language can be challenging. Thereâs frequently a load of novel code patterns, techniques, and features.
Elixir, in particular, is a bit odd. While the syntax looks easy, Elixir code can sometimes be a puzzle even if you are a functional programming veteran.
For this post, I have gone through all the basic Elixir constructs and collected a few tips that will help you write idiomatic Elixir code without hours of Googling. It should be useful when you attempt your first Elixir project or do Exercism exercises.
If you have already finished Exercism or done an Elixir course, you will know most of this already, but it might serve as a good refresher.
Now, letâs see what the Elixir programming language can offer us. đ
đ„ Basic syntax
To learn the basic syntax, just watch this video. Itâs amazingly well done:
Additionally, you can visit the Basic Types and Basic Operators guides on Elixirâs webpage.
đ§ Pipes
The pipe operator is the main operator for function composition in Elixir.
It takes the result of the expression before it and passes it as the first argument of the following expression. Pipes replace nested function calls like foo(bar(baz))
with foo |> bar |> baz
.
Example of pipes
For example, hereâs a function that makes a string into an exclamation sentence.
def exclaim(string) do
string
|> String.trim()
|> String.capitalize()
|> Kernel.<>("!")
end
Here, we take a string and pass it through various string methods. Since Elixir functions return the result of the last evaluated value, we donât have to return anything. Data and transformations applied to it are clearly delineated.
Pipes look even more incredible when you use them to compose higher-level functions. The whole program can easily be one large pipe, consisting of smaller pipes, which consist of even smaller pipes. Pipes all the way down!
âĄïž Higher-order functions
Elixir, as a functional programming language, makes heavy use of higher-order functions.
Higher-order functions take in other functions as arguments. A few examples of HOFs are map, filter, and reduce.
In Elixir, most of these functions live in the Enum module of the standard library and operate on anything that is Enumerable: lists, maps, ranges, etc.
Using HOFs is a good substitute for loops: instead of a for-in, iterate over the elements of the list with Enum.map
.
iex(1)> list = [1, 3, 5, 7]
[1, 3, 5, 7]
iex(2)> Enum.map(list, fn x -> x * x end)
[1, 9, 25, 49]
In the function above, we provided the square function as fn x -> x * x end
. This is Elixirâs syntax for anonymous functions. There is also another way to provide functions, and that is through the capture operator.
iex(3)> Enum.map(list, &(&1 * &1))
[1, 9, 25, 49]
The &
initiates a new anonymous function, while the &1
denotes the first argument of that function. Itâs debatable whether it makes it easier to read in this case. đ
Example of higher-order functions
Letâs say we need to read a list of lines from a file, make them into exclamation sentences, and print them out to the console.
While we could usually do that with a loop, now we need another way. You can easily do it with a map function.
def exclaimify(file) do
file
|> File.read!()
|> String.split("\n", trim: true)
|> Enum.map(fn x -> exclaim(x) end)
|> Enum.join("\n")
|> IO.puts()
end
Remember the capture operator we talked about? Here, you can use it to make it simpler to call exclaim
. Just switch up the map function to Enum.map(&exclaim/1)
!
đ”ïž Pattern matching
Pattern matching is one of the most powerful parts of Elixir. Any kind of control flow that you write, itâs a reasonable heuristic to check whether you canât do it with pattern matching.
Basically, pattern matching works by providing some pattern you expect in the data and then checking whether this pattern matches the data.
It all starts with the match operator, =
. Yup. In Elixir, we donât assign variables; we match them.
iex(1)> x = 1
1
What happens here is that Elixir checks whether the structure of both sides is the same. If it is, unassigned variables on the left side get assigned the values from the right side. If it is not, it will skip to the next match statement (or give you MatchError if the match wasnât exhaustive).
We can play around with it in IEx.
iex(2)> 1 = 1
1
iex(3)> 1 = 2
** (MatchError) no match of right hand side value: 2
iex(5)> x = 1
1
It starts to get much more useful when the structures are more complex, enabling you to destructure tuples, lists, and nested maps quite easily.
Tuples
iex(1)> {x, y} = {1, 3}
{1, 3}
iex(2)> x
1
iex(3)> y
3
iex(1)> {x, y} = {1, 2, 3}
** (MatchError) no match of right hand side value: {1, 2, 3}
Lists
iex(1)> [head | tail] = ["Many", "years", "later", "as", "he", "faced", "the", "firing", "squad"]
["Many", "years", "later", "as", "he", "faced", "the", "firing", "squad"]
iex(2)> head
"Many"
iex(3)> tail
["years", "later", "as", "he", "faced", "the", "firing", "squad"]
iex(4)> [head | tail] = []
** (MatchError) no match of right hand side value: []
Maps
iex(1)> %{age: x} = %{name: "Andrew", age: 27}
%{age: 27, name: "Andrew"}
iex(2)> x
27
Elixirâs case statement operates on pattern matching â you can use it to match values or structures.
def get_andrews_age(person) do
case person do
%{name: "Andrew", age: x} ->
{:ok, x}
%{name: _, age: _} ->
{:error, :not_andrew}
_ ->
{:error, :not_person}
end
end
In the case of the code above, we can go even more idiomatic.
Multiple function clauses
Elixir supports pattern matching in function clauses. Therefore, instead of writing long-winded control flow statements, you can create multiple functions.
def get_andrews_age(%{name: "Andrew", age: x}), do: {:ok, x}
def get_andrews_age(%{name: _, age: _}), do: {:error, :not_andrew}
def get_andrews_age(_), do: {:error, :not_person}
Whoah! đ€Ż If done correctly, this makes the code more extensible and easier to read.
Guards
But thereâs even more modularity incoming.
Letâs imagine we wanted to get the age only if Andrew is of legal age. We can easily accomplish this with a guard, an additional construction that augments pattern matching.
Guards consist of when
and a boolean expression, and are added to the function clause. They support only a limited set of expressions, which you can check out in the documentation.
Hereâs how our code would look with guards:
def get_andrews_age_if_legal(%{name: "Andrew", age: x}) when x >= 18, do: {:ok, x}
def get_andrews_age_if_legal(%{name: "Andrew", age: x}) when x < 18, do: {:error, :not_legal_age}
def get_andrews_age_if_legal(%{name: _, age: _}), do: {:error, :not_andrew}
def get_andrews_age_if_legal(_), do: {:error, :not_person}
Overloading functions
Thereâs this cool thing with Elixir functions where you can have functions with different argument counts, and they get counted as different functions even though they have the same name. In other words, you can overload functions via arity.
def add(x,y), do: x + y
def add(x,y,z), do: x + y + z
One of these functions in Elixir-speak is actually add/2, the other is add/3.
And, by using multiple functions and pattern matching, we can do a nice trick in our Elixir code that comes up quite often.
We can define an x-argument function that will serve as the outer shell of our function. Then, we use that x-argument function to launch an x+1-argument function that will track the state as an argument, not a variable.
Itâs easier if you look at it in action. Hereâs a function that reverses a list.
def reverse(list), do: reverse(list, [])
def reverse([], list), do:
def reverse([head | tail], reversed_list), do:
I have left the implementation details unfinished, and you might have a question or two about what is happening. Stay with me for a while. To fully explain it, we need to turn to recursive functions.
â° Recursion
A recursive function is simply a function that can call itself.
One of the typical examples is calculating a certain Fibonacci number, a naive implementation of which in Elixir would be:
def fib(0), do: 0
def fib(1), do: 1
def fib(n), do: fib(n-1) + fib(n-2)
(Take note of our use of pattern matching covered above.)
We can find two things in each sane recursive piece of code: a base case and a recursive call.
- Base case. This is the point where recursion terminates. Itâs the result you want to achieve, and it will not call itself.
- Recursive call. This is where the program will do most of the computation. With it, you will reduce your task to a smaller one, and it will call itself.
Basically, you can think of the base case as the simplest possible example of the task you want to solve and the recursive call as a way to simplify any given problem. In the example given, fib(0)
and fib(1)
are base cases, fib(n)
is the recursive call.
Looping with recursion
In Elixir, we use recursion much more frequently than you would in a non-FP language since we donât have access to things like loops. Recursion mimics imperative behavior quite well.
For example, if we need to do something a set amount of times, we can create a recursive function with a number as an argument.
Letâs say that we want to duplicate a string any given number of times.
def repeat(string, times), do: repeat(string, times, "")
def repeat(_string, 0, acc), do: acc
def repeat(string, times, acc), do: repeat(string, times - 1, acc <> string)
Instead of initializing a variable in the function, we add one more argument to the function. Then we call it again and again, each time decreasing times
and increasing acc
.
In the end, we have achieved the same result as instantiating two variables â i
and acc
â and creating a while loop that decrements i
until it is equal to 0 and adds string
to acc
on each iteration.
Now, I believe you will find it easy to complete the reverse
example in the pattern matching section; itâs rather similar to repeat
in structure.
đ Structs
Structs are an upgrade of Elixirâs default key-value store, maps. In contrast to maps, the keys of a struct are set when initializing, and you canât add new ones afterwards.
Each module can have one and only one struct, and we tend to define it somewhere at the top of the module.
defmodule Person do
defstruct [:name, :age]
end
In the example above, I will now be able to access the struct with %Person{}
. I can also provide some default values. For example, letâs assume that the default population I am dealing with, for some reason, is 27-year-old Andrews.
defmodule Person do
defstruct [name: "Andrew", age: 27]
end
Now, calling the struct will give me a named map with the default values.
iex(1)> %Person{}
%Person{age: 27, name: "Andrew"}
The keys of the structs are not changeable once defined. Therefore, you wonât be able to add additional facts to the Person struct such as a home address, favorite Dostoevsky novel, and other tidbits that nobody needs to know about.
But you can easily instantiate a struct with values other than default, update the values with the regular map functions, etc.
You can read more about structs here.
Using structs
While creating a simple game in Elixir, you can, for example, create a struct that encodes the game state. Then, you provide functions that modify the game state in the module below.
Exercism has a wonderful example in the robot simulator exercise. In it, you create a struct that holds the position and direction of a robot and provide various functions for moving and turning the robot.
One could argue that the modules created like this, in a sense, function similarly to a class minus all the inheritance stuff (which we donât do in Elixir, but there are ways to achieve behavior like that).
đșïž What to learn next?
This knowledge (and a bit of Googling, I definitely didnât cover everything) should be enough for you to go through Exercism and be able to do simple programs in Elixir.
If you want to continue learning after that, we have a huge article on all the different resources you can use. On the practical side, I recommend building a few toy projects for whatever interests you, perhaps looking into Phoenix, the main Elixirâs web framework. I mean, you know thyself better. đ
Additionally, if you want to read more articles about Elixir and other fantastic languages, be sure to follow us on Twitter. Good luck!