Elixir, the path to functional programming ⚗️
With my Rust rediscovery a lot of functional programming concepts became less obscure. In order to go deeper into this paradigm I chose Elixir a powerful general purpose, functional programming language, compiled and executed inside the Erlang virtual machine (BEAM).
But why Elixir, if Erlang exists ? Because :
Elixir, is Erlang with underpants - Athoune
Unlike Erlang, the Elixir syntax is clear and elegant. The language comes with everything you need, included : interactive shell, deps tooling, building tooling and more…
Even if José Valim, the Elixir creator was a core Ruby dev, Elixir is
completely different since this is a functional programming language. No
mutation, no inheritance, no classes, only pure functions. Pray your only
true god, the pipe operator |>
operator, used to compose functions.
Now, I know, the only thing you want is code and examples, so let’s take a quick look at some basic Elixir stuff using the interactive shell IEx.
A simple addition :
iex(1)> 2 + 2
4
A simple addition, inside a list
iex(1)> 2 + 2
iex(5)> list = [1,2,3,4][1, 2, 3, 4]
iex(6)> Enum.map(list, fn e -> e + 1 end)
[2, 3, 4, 5]
List splitting (head and tail)
iex(7)> [head | tail] = list
[1, 2, 3, 4]
iex(8)> head
1
iex(9)> tail
[2, 3, 4]
Another important aspect of Elixir is pattern matching used to match values, data structures and much more, let’s try it :
iex(11)> x = {:this, :is, :a, :test}
{:this, :is, :a, :test}
iex(12)> {a, b, c, d} = x
{:this, :is, :a, :test}
iex(13)> a
:this
iex(14)> b
:is
iex(15)> c
:a
iex(16)> d
:test
Using pattern matching you can assign values but also destructure data to simplify interaction with it.
Feels the hype growing ? Nice !
As usual, I like a real project to experiment on something. In the Elixir case, I started a Discord bot project called o2m. The main idea of this bot is to send alert messages on a specific channel when a new episode from a selected podcast is available. Today, I was struggling with a bug on a production instance of o2m. The last episode was not fetched correctly and the action that write a “There is a new episode” message was not triggered correctly. To debug and inspect the current state of the application I used IEx in production.
Base ingredient of a good Elixir : Mix 🧙
Combined with IEx, there is Mix. Shipped with Elixir, this a build tool used for the following application related tasks :
- creating
- setting up needed deps
- testing
- compiling
The combo killer here is to start IEx inside the Mix project, in order to have all the dependencies imported inside the interactive shell
iex -S mix
After that, there is a lot of useful commands like recompile
to refresh and
recompile new code while your code is running. Yes, this is something
that Elixir do by default, hot code reloading.
When your code is ready, you want to deploy it. With Mix the standard way
is to used the release
command
MIX_ENV=production mix release
This will create a precompile and packaged unit, with runtime included. With this, you do not have to install Erlang or Elixir on your production server. Boom.
If you look carefully, there is an interesting message and the end of the command output :
Release created at \_build/prod/rel/o2m!
# To start your system
_build/prod/rel/o2m/bin/o2m start
Once the release is running:
# To connect to it remotely
_build/prod/rel/o2m/bin/o2m remote
# To stop it gracefully (you may also send SIGINT/SIGTERM)
_build/prod/rel/o2m/bin/o2m stop
To list all commands:
_build/prod/rel/o2m/bin/o2m
Particularly,
To connect to it remotely
This means I can have access to an interactive shell on production while my code is running, hooray 🎆
Sherlocking a GenServer 🔎
Did you read the title ? We are talking about GenServer here ! So what the fuck is this ?
Taken from the documentation :
A GenServer is a process like any other Elixir process and it can be used
to keep state, execute code asynchronously and so on. The advantage of using
a generic server process (GenServer) implemented using this module is that it
will have a standard set of interface functions and include functionality for
tracing and error reporting. It will also fit into a supervision tree.
In Elixir, this is the default and common module used to implement client-server behaviors.
Here is the example from the documentation
defmodule Stack do
use GenServer
# Callbacks
@impl true
def init(stack) do
{:ok, stack}
end
@impl true
def handle_call(:pop, \_from, [head | tail]) do
{:reply, head, tail}
end
@impl true
def handle_cast({:push, element}, state) do
{:noreply, [element | state]}
end
end
Basically, this is a data structure with a state responding to triggers using handlers that update or retrieve the current state
To launch and interact with the server, in IEx :
iex(41)> {:ok, pid} = GenServer.start_link(Stack, [:hello])
iex(42)> GenServer.call(pid, :pop)
:hello
iex(43)> GenServer.cast(pid, {:push, :world})
:ok
iex(44)> GenServer.call(pid, :pop)
:world
What if, I want to access the current state, of the GenServer ?
In Erlang, there is the sys
module with the get_state/2
function available
But, this is Erlang, and we use Elixir ! We’re doomed ? No ! Because
everything available in Erlang is available inside Elixir using the :
operator.
iex(18)> :io.format("Hello World~n")
Hello World
:ok
So, with our sys
example :
iex(19)> :sys.get_state(pid)
[:!, :world]
Nice ! Now we can get state of a GenServer using the PID !
Now, let’s move from local machine to prod. In this case, processus are handled by a Supervisor but first things first, we want a IEx shell inside our running app. In order to illustrate this last part, I will be using one of my prod instances of o2m
o2m@16557a733b59:/opt/o2m\$ ./prod/rel/o2m/bin/o2m remote
Erlang/OTP 22 [erts-10.5.3][source] [64-bit][smp:2:2] [ds:2:2:10][async-threads:1] [hipe]
Interactive Elixir (1.9.2) - press Ctrl+C to exit (type h() ENTER for help)
iex(o2m@16557a733b59)1>
Ok but, what is the pid of the GenServer I want to inspect ? I don’t know ! Here the first solution could be outputting pid to stdout but, imagine that our application kills and restart GenServer, on demand or our application is flooding stdout because of some errors, this is not a viable solution.
First, we could list all processus,
iex(o2m@16557a733b59)3> Supervisor.which_children(O2M.Supervisor)
[
{O2M, #PID<0.2368.0>, :worker, [O2M]},
{"jobs-https://feed.ausha.co/bj5li17QPONy", #PID<0.2365.0>, :worker, [Jobs]},
{"jobs-https://feed.ausha.co/yJeEUGlLVq0o", #PID<0.2362.0>, :worker, [Jobs]},
{"jobs-https://anchor.fm/s/b3b7468/podcast/rss", #PID<0.2358.0>, :worker,
[Jobs]}
]
Where O2M.Supervisor
is the dedicated Supervisor of my application
Now I can identify the GenServer I want to inspect, let’s take the
jobs-https://anchor.fm/s/b3b7468/podcast/rss
one, with associated pid
0.2358.0
(the last element of the list, here), here comes the fun
iex(o2m@16557a733b59)13> {_, pid, _, \_} = Supervisor.which_children(O2M.Supervisor) |> List.last
{"jobs-https://anchor.fm/s/b3b7468/podcast/rss", #PID<0.2358.0>, :worker,
[Jobs]}
iex(o2m@16557a733b59)14> pid
#PID<0.2358.0>
Then, I can use the pid value to get GenServer state and inspect it to see if everything is ok :
iex(o2m@16557a733b59)15> :sys.get_state(pid)
{"https://anchor.fm/s/b3b7468/podcast/rss",
%{
date: "Mon, 04 Nov 2019 09:00:00 GMT",
show: "Harry Cover, le podcast des meilleures reprises",
title: "Yes we can work it out !",
url: "https://anchor.fm/leotot8/episodes/Yes-we-can-work-it-out-e8n4d3"
}}
How ! Impressive ! It was a little bit Sherlock Holmes oriented debugging but that was fun. Keep in mind that this is a really simple operation here, from IEx everything is possible from starting new GenServers to modify state of a specific one and much more.
I said it at the beginning, Elixir is powerful.