Elixir at a Glance — A Language a Day, Advent Calendar 2019 Day 12/24

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

Leave a Reply

Your email address will not be published. Required fields are marked *