Starting Elixir
Elixir has been around for a while and has proven itself as a language with enduring potential, backed by a great community that it is making active development to even promote Machine Learning workflows in elixir with great tools like Nx, Explorer and Bumblebee.
But keeping an eye on elixir, what does it offers to us?
- Data Immutability: Variables cannot be overwritten; they can only be copied or created with new values, eliminating concerns like mutexes, semaphores, and race conditions.
- Concurrency: Elixir uses lightweight threads of execution (called processes).
- Scalability: Elixir’s processes and it’s way of communication allow us to scale systems easily, either horizontally (adding more machines to the cluster) or vertically (using the available resources of the machine more efficiently).
The fact that it provides data immutability and concurrency, allows it to fully take advantage of concurrency and parallelism, for speeding up a lot of workflows in a easy manner.
In this post, we will be seeing a basic usage of Elixir Language, and how to solve some basic algorithms with it.
LiveBook
The suggested way to test elixir is by using the Livebook software which is like a Jupyter notebook software but in elixir ecosystem.
Variables and Pattern Matching
Let’s start with some basic usage of variables and its usage:
1
2
3
4
x = 1
x
# => 1
Here we declared a simple x
variable with value 1, now we can do Pattern Matching of this variable to value 1
in this case:
1
2
3
4
5
6
7
8
1 = x
# => 1
# Now the next match will break
2 = x
# ** (MatchError) no match of right hand side value: 1
# (stdlib 5.1.1) erl_eval.erl:498: :erl_eval.expr/6
# #cell:ns25t66h6jhiekh5:1: (file)
As we can see, the second match will break because the variable x
doesn’t match the 2
.
If we try to match a value 1
with an non-existing variable, it will also break:
1
2
1 = whatever
# error: undefined variable "whatever"
Now, we can do some more elaborated Pattern Matching, in this case with a tuple of three elements:
1
2
3
4
5
6
7
8
9
data = {:hello, true, 1}
{a, b, c} = data
IO.inspect(a)
# => :hello
IO.inspect(b)
# => true
IO.inspect(c)
# => 1
Here, we are extracting the values contained in the variable at the right by using pattern matching.
In the next piece of code, it will fail because the structure doesn’t Match.
1
2
3
4
{a, b, c} = {:hello, "world"}
# ** (MatchError) no match of right hand side value: {:hello, "world"}
# (stdlib 5.1.1) erl_eval.erl:498: :erl_eval.expr/6
#cell:c6qv6iqrk3vtevuv:1: (file)
Note: the variable :hello
is an atom, like ruby atoms that are loaded permanently in memory for faster usage.
Matching Lists
Lists have some special matching cases
1
2
3
4
5
6
7
8
a = [1, 2, 3]
[head | tail] = a
IO.inspect(head)
# => 1
IO.inspect(tail)
# => [2, 3]
With the [head | tail]
syntax, we can extract the first value of a list in a variable in this case called head, and then keep the rest of the list in the other variable called tail.
For the case of a list with one element:
1
2
3
4
5
6
7
8
a = [1]
[head | tail] = a
IO.inspect(head)
# => 1
IO.inspect(tail)
# => []
We can identify a stop condition when the tail get’s the form of a empty list []
.
Here it will break because the list it is already empty:
1
2
3
4
5
6
a = []
[head | tail] = a
# ** (MatchError) no match of right hand side value: []
# (stdlib 5.1.1) erl_eval.erl:498: :erl_eval.expr/6
# #cell:s2tx2qzu6ul44zkb:3: (file)
You may want to ignore parts of a match, that work for “_” underscore character:
1
2
3
4
5
6
7
a = [1, 2, 3]
[head | _] = a
head
# => 1
The Pin operator
For now we have seen only a way of doing match with hard coded values, or using variables to read information inside tuples or lists. But what if we want to do a match with a value inside a variable?
1
2
3
4
x = 1
^x = 1
# => 1
Here we are doing a match of the variable contained in x
with the 1 at the right, instead of doing variable assignation.
In this case it will fail:
1
2
3
4
5
6
x = 1
^x = 2
# ** (MatchError) no match of right hand side value: 2
# (stdlib 5.1.1) erl_eval.erl:498: :erl_eval.expr/6
# #cell:m52ee3dhdsr7eeuq:3: (file)
Basically it is useful when you need to match dynamically for a certain value, it is highly used with Ecto queries which is a famous package for SQL in elixir.
Another example:
1
2
3
4
5
6
7
8
9
x = 1
a = [1, 2, 3]
[^x, b, c] = a
{b, c}
# => {2, 3}
Maps
Now, let’s see a bit of maps
1
2
3
4
5
6
7
8
9
10
user = %{"username" => "foo", "role" => "admin"}
user["username"]
# => "foo"
# Atoms and access to them
user = %{username: "foo", role: "admin"}
{user.username, user.role}
# => {"foo", "admin"}
Data Immutability
Depending of the scope, variable will have a value or another value:
1
2
3
4
5
6
7
8
9
10
a = 1
if a == 1 do
a = 2
IO.puts(a)
# => 2
end
IO.puts(a)
# => 1
This is because whenever a variable get’s assigned (Eg: a = 1
or a = 2
) a copy is created, the memory will hold a new variable. In the initial scope, a
var get value 1
, in the scope inside the conditional, a new a
is assigned with value 2
, and after the scope, the original copy of a
remains with value 1
.
Consider another example involving map operations:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
map_a = %{"foo" => "bar"}
IO.inspect(map_a)
# => %{"foo" => "bar"}
map_b = Map.put(map_a, "baz", "foo")
IO.inspect(map_b)
# => %{"baz" => "foo", "foo" => "bar"}
map_c = Map.delete(map_b, "baz")
IO.inspect(map_c)
# => %{"foo" => "bar"}
# Returning the modified data structure is a common pattern
{val, map_d} = Map.pop(map_c, "baz")
IO.inspect({val, map_d})
# => {"bar", %{}}
Notably, many data structure operations in Elixir return the modified structure as part of the response, emphasizing data immutability.
A common Pattern Match example
Now, let’s see a code that is more common to use:
1
2
3
4
5
6
# Here I'm using the "req" package.
{:ok, response} = Req.get("https://api.github.com/repos/wojtekmach/req")
response.body["description"]
# => "Req is a batteries-included HTTP client for Elixir."
Handling different responses with a case statement:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# https://postman-echo.com/test always returns a 404 response
case Req.get("https://postman-echo.com/test") do
{:ok, %{status: status, body: body}} when 200 <= status and status < 300 ->
body
{:ok, %{status: status}} ->
"non ok response with code #{status}"
{:error, error} ->
"an error happened #{inspect(error)}"
end
# => "non ok response with code 404"
In the last example, we can see the case
operator, which will evaluate the return of Req.get("https://postman-echo.com/test")
and will go through the corresponding match case, in the example, the second case because for the initial the status is not between 200 and 300.
The Pipe Operator
Now let’s dive into the |>
pipe operator:
1
2
3
4
5
6
# ((1 + 2) ^ 2 ) * 2
# (3 ^ 2 ) * 2
# 9 * 2
# 18
1 |> Kernel.+(2) |> :math.pow(2) |> Kernel.*(2)
# => 18.0
Basically the pipe operator allows us to take the response from the last operation as the first argument of the next operation.
Another example using maps:
1
2
3
4
5
6
7
a_map = %{"remove_me" => "wololooo"}
# Last output will be first argument of next operator
new_map = a_map |> Map.put("foo", "bar") |> Map.put("baz", "asdf") |> Map.delete("remove_me")
IO.inspect(new_map)
# => %{"baz" => "asdf", "foo" => "bar"}
Functions
Functions always go inside modules:
1
2
3
4
5
6
7
8
defmodule MyMod do
def sum(a, b) do
a + b
end
end
MyMod.sum(1, 2)
# => 3
Default arguments can be defined as follow:
1
2
3
4
5
6
7
8
9
10
11
defmodule SayHi do
def say_hi(msg \\ "Hi") do
msg
end
end
hi1 = SayHi.say_hi()
hi2 = SayHi.say_hi("Hello")
{hi1, hi2}
# => {"Hi", "Hello"}
There is a @spec
syntax that allows us to define the specification of a function. Elixir is not a typed programming language, this works mostly for certain code analysis tools like dialyzer.
1
2
3
4
5
6
7
8
9
10
11
12
13
defmodule MyTypedMod do
# First argument is a number (either float or integer). Second is an integer. The result is a number.
@spec sum(number, integer) :: number
def sum(a, b) do
a + b
end
end
MyTypedMod.sum(1.1, 2)
# => 3.1
MyTypedMod.sum(1, 2)
# => 3
Functions and Pattern Matching
You can use Pattern Matching when defining functions, this allow us to have better explained what a function does:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
defmodule Calculator do
def calc("+", a, b) do
a + b
end
def calc("-", a, b) do
a - b
end
def calc("*", a, b) do
a * b
end
end
IO.puts(Calculator.calc("+", 1, 2))
# => 3
IO.puts(Calculator.calc("-", 1, 2))
# => -1
IO.puts(Calculator.calc("*", 1, 2))
# => 2
Recursion
Now let’s see a bit of recursion in elixir:
1
2
3
4
5
6
7
8
9
10
11
12
13
defmodule Recursion1 do
def repeat(msg, times \\ 3) do
if times > 0 do
IO.puts(msg)
repeat(msg, times - 1)
end
end
end
Recursion1.repeat("Hello")
# => "Hello"
# => "Hello"
# => "Hello"
This code will print three times "Hello"
and will return nil
after it, for the case times < 0
it will return nil.
Recursion and Pattern Matching
We can use both recursion and pattern matching at the same time an do something like:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
defmodule Recursion2 do
def repeat(msg, times \\ 3)
def repeat(_msg, 0), do: true
def repeat(msg, times) do
IO.puts(msg)
repeat(msg, times - 1)
end
end
Recursion2.repeat("Hello")
# => "Hello"
# => "Hello"
# => "Hello"
# => true
This code does the same as the previous but it has a better defined repeat
function, Here is important that the stop case is before the recursive loop (the case it is defined like def repeat(_msg, 0), do: true
)
Here is another example with lists and recursion:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Lists and Recursion
defmodule ListsRecursion do
def sum([]), do: 0
def sum([head | tail]) do
head + sum(tail)
end
end
ListsRecursion.sum([1, 2, 3])
# => 6
# Alternatively, leverage built-in libraries
Enum.sum([1, 2, 3])
# => 6
Conclusion
We have seen some elixir basic fundamentals of how to handle information. There are more concepts that you should learn how to use to efficiently code in elixir, concepts like processes, Agents, GenServers, map and reduce with Enum or Stream modules, etc. If you are new to elixir you can continue with the Official Elixir introduction.
Resources
- https://elixir-lang.org/docs.html Elixir website
- https://hexdocs.pm/elixir/1.16/introduction.html Official Elixir introduction
- https://github.com/sonic182/starting_elixir/blob/main/starting_elixir.livemd A Livebook (Notebook like) from a Meetup I did.