About this ‘A Language a Day’ Advent Calendar 2019
Welcome to Day 12 of this year’s A Language a Day Advent Calendar. Today’s topic is introduction to the Elixir programming language.
Facts about the language
Some facts about the Elixir programming language:
- Based on Erlang, and using its virtual machine
- A functional language
- Supports concurrent execution
- Appeared in 2011
- Website: elixir-lang.org
Installing and running Elixir
Installing Elixir is easy. The website contains a lot of one-liner instructions for many operating systems. For example, to install it on Mac, type:
$ brew install elixir
You can now use either a REPL shell called iex
or a standalone compiler elixir
. By convention, file name extension is .ex
for the files that you compile, and .exs
for the files that you intend to use as scripts.
To run a program, type:
$ elixir hello-world.exs
Hello, World!
Here is the minimum program that prints a message to the console.
IO.puts "Hello, World!"
This program uses the IO
module and its function puts
.
Variables
When talking about variables, you have to remember that you do not assign values to them. You match the two values. This is how you set a value to the name
variable using the match operator =
.
name = "John" IO.puts "Hello, #{name}!"
You can also see how the variable is interpolated in the string : #{name}
.
Just to see how you can concatenate stings, here is the same example without variable substitution:
IO.puts "Hello, " <> name <> "!"
Lists
List is an important data type, so let us quickly look at it. Lists are written in a pair of square brackets:
data = [10, 20, 30]
You can split a list into its head and the tail with a vertical bar:
[head | _tail] = data IO.puts head # 10
As you can see, the match operator works here again. What stands on its left side does not have a value yet, so both the head
and the _tail
variables are receiving a value. You get the first element of the list in the head
variable, and the rest of it is put into _tail
. The underscore is used to prevent the warning that this variable is not used (you can use a bare _
too).
You can append another list to your list using the ++
operator, or you can subtract it with --
. Examine the following code to see how it works:
data = [10, 20] ++ [30, 40] # data is now [10, 20, 30, 40] [h | t] = data -- [10, 40] # data is [20, 30] IO.puts h # 20 IO.inspect t # [30]
Notice that when you split a list into the head and the tail, the head is a single element, while the tail is a list, even if it contains a single element. To print a list to see its contents, use IO.inspect
instead of IO.puts
.
Functions
Surprisingly, anonymous functions (lambdas) are much simpler to start with in Elixir. Use the pair of fn
and end
keywords to define the signature and the body of a function. You split them with an arrow, as shown in the following example of evaluating the circumference for the given radius. A function is a value, so you can save it in a variable.
circumference = fn (r) -> 2 * :math.pi * r end IO.puts circumference.(10) # 62.83…
Notice that to call this function you not only need to have parentheses, but also a dot before them: circumference.(10)
.
The .()
construction is also required when your function takes no arguments:
tell_name = fn -> IO.puts "Elixir" end tell_name.()
Named functions
To have a named function, you need to define it within a module.
defmodule MyModule do def circumference(r) do 2 * :math.pi * r end end
The name of the module starts with a capital letter. Having this done, you can continue with using the function by specifying its fully-qualified name:
IO.puts MyModule.circumference(10) # 62.83…
Multiple bodies
You can implement different behaviour of a function in response to different incoming parameters. Here is an example of a function that returns the next colour of a traffic light based on the currently active signal.
next_color = fn (:red) -> :green (:green) -> :yellow (:yellow) -> :red end IO.puts next_color.(:yellow) # :red IO.puts next_color.(:green) # :yellow IO.puts next_color.(:red) # :green
The constructs like :red
are the so-called atoms. This is a value that you can use like any other values. In the case of this function, atoms are much more preferable than using text strings, for example.
You simply have more than one ->
in such a function. It is not possible to have multiple dispatch based on the number of the parameters, but you can, for example, give a special formula for the given values:
f = fn (0) -> 100 (n) -> 2 * n end IO.puts f.(0) # 100 IO.puts f.(-5) # -10 IO.puts f.(6) # 12
A Factorial example
Despite the first attempt to write an anonymous recursive function for computing a factorial, you cannot do it, as there is no function name to call for going to the next recursion level. So, let us create a named function that calculates a factorial recursively and uses multiple functions for different argument values.
defmodule Factorial do def f(1) do 1 end def f(n) do n * f(n - 1) end end IO.puts Factorial.f(1) # 1 IO.puts Factorial.f(5) # 120 IO.puts Factorial.f(7) # 5040
There is a lot more of what you can do with functions, but let us move on to the next topic.
A Polymorphic example
Elixir has no support for object-oriented programming. Let us see what we can do to implement the traditional zoo program that prints different messages for dogs and cats. To make it a bit more difficult, let us add names to the animals.
My solution is shown below. I am using the tuples to keep the kind of an animal and their pet nicknames. You create a tuple with a pair of curly braces. For the kind of an animal, I am using atoms as we did it in one of the earlier examples with the traffic lights colours.
defmodule Animal do def info({:dog, name}) do "#{name} is a dog." end def info({:cat, name}) do "#{name} is a cat." end end zoo = [ {:dog, "Charlie"}, {:dog, "Milo"}, {:cat, "Gracie"}, {:cat, "Molly"} ] for x <- zoo do IO.puts Animal.info(x) end
The Animal.info
function has two implementations. Elixir chooses the one that has a correct match. Each implementation demands the first element of a tuple to be either :dog
or :cat
, and the second parameter carries the name to the result value.
In this program, we are using the for
loop for the first time in this article. For every element in zoo
, the same code is called: Animal.info(x)
.
The output of the program is the following:
Charlie is a dog Milo is a dog. Gracie is a cat. Molly is a cat.
Concurrency
Elixir runs on the Erlang’s virtual machine, which in its turn supports the so-called processes, which are not processes in sense of operating system. It allows running concurrent code, and if possible even on multiple cores.
To create such a process, spawn
it.
defmodule MyModule do def f(x) do IO.puts x end end spawn(MyModule, :f, [42]) :timer.sleep(100) # milliseconds
The spawn
function receives the name of the module, the name of the function in it, and the list with the arguments that will be passed to the function. It is your task to wait until the process is done.
Sleep Sort
Let us first implement the algorithm using the spawned processes only.
defmodule SleepSort do def sort_number(n) do :timer.sleep(n * 100) IO.puts n end end data = [10, 4, 2, 6, 2, 7, 1, 3] for n <- data do spawn(SleepSort, :sort_number, [n]) end :timer.sleep(5000)
This code works similarly to the previous example. It creates a separate process (in Elixir terms) for each number in the data
list. To wait for them all to finish, a big enough delay has been added at the end of the program.
Let us modify the code to avoid an arbitrary delay. We can use the messaging system available in Elixir.
defmodule SleepSort do def sort_number(parent, n) do :timer.sleep(n * 100) send(parent, n) end def process(count, total) do receive do n -> IO.puts n if count < total do SleepSort.process(count + 1, total) end end end end data = [10, 4, 2, 6, 2, 7, 1, 3] for n <- data do spawn(SleepSort, :sort_number, [self(), n]) end SleepSort.process(1, length(data))
The main communication goes through send
on one end and receive
on the other. Notice how the receiving loop is organised. This also demonstrates how you organise loops recursively.
The SleepSort.process
function receives two arguments: the number of the current iteration and the total number of expected calls. While the number of iterations is small enough, the function is called again from itself. A receive
call will wait until there is a piece of information in the communication channel. Its first argument, parent
, receives the PID of the main process obtained by self()
.
Get more
Functional programming is partially a different world for many programmers. If you like it, you can go further.
It may also be useful to get some knowledge of Erlang.
The source files of today’s examples in Elixir are located on GitHub.
Next: Day 13. OCaml