Taste of Elixir - part 1

productivity
elixir
Author

Erik Lundevall-Zara

Published

September 11, 2024

In BEAM me up, Elixir I wrote about the BEAM virtual machine and the lovely language Elixir. However, I did not provide any code examples for Elixir. In this post I will show a little taste of some nice features of the language which I find quite useful, and I hope you will like it as well! This is not a tutorial for Elixir, and I assume that you already know how to write code in at least one other programming language.

I will talk about a few features of Elixir, including:

In the next part of Taste of Elixir, I will go through some examples of simple data processing in Elixir.

Hello, world!

As in many introductions to programming languages, we are going to write a short snippet of code to write a program that prints “Hello, world!”.

IO.puts("Hello, world!")

This single line of code calls a function called puts, which is part of a module called IO. This module is part of the standard library. In Elixir, we organise code in modules. Any function you write is part of a module. So if we want to define a function called hello, we need to write something like this:

defmodule Hello do
  def hello do
    IO.puts("Hello, world!")
  end
end

Here, we define a module called Hello, and inside that module, we define a function called hello. We can the call that function by writing the name of the module followed by the dot and the name of the function:

Hello.hello()

which will give the output:

Hello, world!

Pattern matching, recursion and lists

Let us go for a bit more involved example.

We want to write a function called repeat, which will take an item and a count n as input, and create a list of that item n times, and return that list. Note that we have not specified what kind of item we are going to repeat.

The caller of the function should be able to use anything, including other lists or complex data structures, not just simple integers or strings. In Elixir, we do not need to specify the type of the item we are going to repeat. The language uses dynamic, strong typing, so we can define functions without specifying the parameter types.

We are going to solve this problem one step at a time, and introduce some features of the language along the way. First, let us just define the shape of the function, what parameters there will be.

We have two parameters here, called item and count. We are going to repeat the item item, count times, and return a list with that item in it.

Let us start with the simplest case, which would be to repeat something 0 times. First, we define a new module called Taste.Repeat1. In Elixir we can define name hierachies for modules, so we can use Taste.Repeat1 as a module name.

defmodule Taste.Repeat1 do
  def repeat(item, count) do
    []
  end
end

That will always return an empty list.

In Elixir, [] represents the empty list.

The value of the last expression in a function is the return value, there is no explicit return statement needed. Compiling this code will generate warnings about unused variables because of the parameters not being used.

We can start the names of variables with an underscore, as in _item and _count. This instruction will inform the compiler that the variables are not being used. We can also write the function in a shorter form in this case on a single line:

def repeat(_item, _count), do: []

For brevity, I have omitted the surrounding module definition, but that should still be there for working code. Now it is time to apply some Elixir pattern matching to the function parameters.

Since this case is correct when the count is 0, we can specify the count parameter to be 0.

def repeat(_item, 0), do: []

This function definition will work just fine and compile, and as long as the count is 0, calling the function will also work.

So what do we do if the count is not 0?

We add another version of the function, we can do a recursive approach.

def repeat(item, count) do
  [item | repeat(item, count - 1)]
end

Here, we use Elixir syntax to make a list which comprises one item in the beginning, and another list as the rest of that list, which we get from a recursive call to our repeat function, with the count value decreased by 1.

We will apply the version of the function that covers the case when the count is 0, since we already have it, when the recursive call is 0. But since we do not have any types specified, what happens if the count is negative, or not an integer?

Here, we want to raise an exception, since we do not have any meaningful result for those cases.

You can also add this as a separate function definition with a guard clause using the keyword when.

def repeat(_item, count) when not is_integer(count) or count < 0 do
  raise "Invalid count #{count}"
end

As you can see here, we have a very simple case of pattern matching of function calls, which allows us to express the logic of the function in a quite readable manner.

It is a feature which can make code more declarative and easier to reason about. I think it also fits better with how we think when trying to break down a problem into smaller and more manageable pieces. This is the definition of repeat as it stands now:

defmodule Taste.Repeat1 do
  def repeat(_item, count) when not is_integer(count) or count < 0 do
    raise "Invalid count #{count}"
  end
  def repeat(_item, 0), do: []
  def repeat(item, count) do
    [item | repeat(item, count - 1)]
  end
end
Taste.Repeat1.repeat(1, 4)

[1, 1, 1, 1]

Taste.Repeat1.repeat(["a", "b", "c"], 3)

[["a", "b", "c"], ["a", "b", "c"], ["a", "b", "c"]]

But let us now tweak this function a bit.

While the code works fine, what happens if we set the count to be really high?

Besides the obvious part that there will be a very long list in memory, there is also the concern that there will be many function calls waiting for the results from a recursive call, before they can produce their own result.

This is because the result of the recursive call is not the last computation in the function.

If the recursive call had been the very last action, then the compiler could optimize away the waiting of previous function calls, which is referred to as “tail recursion”.

In order to make this work, we need to pass the state of our list building in the recursive calls, a third parameter. We do not want to expose that, so we can make this variant private to the module - no one outside of the module will access them. If we use the keyword defp instead of def, we can make the function definition privater to that module.

With this approach, we build the list as we do the recursive calls, and when we get to the innermost recursive call, we simply return the list we have built. Since the function definitions have different number of parameters, Elixir will consider them different functions, and there will not be any clashes between the public and private functions here, even though they have the same name. repeat

defmodule Taste.Repeat2 do
  def repeat(_item, count) when not is_integer(count) or count < 0 do
    raise "Invalid count #{count}"
  end
  def repeat(item, count) do
    repeat(item, count, [])
  end
  
  defp repeat(_item, 0, acc), do: acc
  defp repeat(item, count, acc) do
    repeat(item, count - 1, [item | acc])
  end
end

Wrapping up part 1

You may ask, could this have been done with some loop that inserted the items into a list?

Elixir is a language that works with immutable data, so if you have created a data structure, you cannot change it. This has some clear benefits in that you can always be sure that the data that you reference will never change by some other part of the code unexpectedly.

This is very important for safety, in particular in software with concurrent execution, and also general troubleshooting. Elixir uses some internal representation of data to optimize the performance when using immutable data structures, so there will not be any excessive copying of data. It will provide a peace of mind when working with data structures, to rely on the immutable data property.

I hope that this first taste may have made you interested in exploring Elixir in more detail. If so, feel free to continue to Taste of Elixir part 2!

For more information about Elixir, check out the Elixir language website. Also, check out the Elixir track at Exercism: https://exercism.org/tracks/elixir

If you want to play around with Elixir without installing it, you can try a playground: https://playground.functional-rewire.com

I can also recommend to install Livebook, which is a great way to play around with Elixir.

Happy coding!

Back to top