Post

Starting Elixir

Image by benzoix

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