My venture into Alchemy

Elixir.

Coding in Elixir.

That’s what I meant by Alchemy.

I decided to jump into yet another language over the holidays. It’s the perfect time to do so; back at the family home in sweltering Namibia. As it is, most of the city of Windhoek’s population are away at the coast like sensible people so there isn’t much to do (especially since I watched TLJ before coming home).

Why Elixir?

Why not? I’ve found myself quite enamoured by Elm and as time went on I kept coming across links back to the Phoenix web framework; it being cited as an ideal server-side companion to Elm apps. I had decided to jump right into Phoenix and figure stuff out as I go along, but in my attempt to do so I realised that there’s more to the language behind Phoenix than simply another web backend generator. So, instead I decided to start with Elixir.

Having made my way through the official Elixir guide I tried to find a simple project I could code in an hour or two. I settled on a roman numeral to decimal converter. Given the relative simplicity of the code, I figured I’d use a script rather than a fully fledged executable.

The code is available on Gitlab as a snippet and Github as a gist (inlined below). This rest of this article highlights some of the basic concepts of Elixir used to write the converter.


Language features

Before getting into the details, let’s go over some ground work.

Types

  • :atom An atom is similar to a Symbol in other languages. It is a constant whose name is it’s value. In javascript, an atom could replace all the const name = "name" code (looking at you Redux action types). Atoms are used all over Elixir projects, particularly for pattern matching and destructuring tuples.
  • {:ok, "this", 42} This is a tuple. They store elements of any type contiguously in memory. Quite handy for “loaded” return values that have some metadata attached to the actual value being returned.
  • [3, 4, 5] A linked list. Common in most languages as lists or arrays. Each element contains a reference to the next element.

Pattern matching

Pattern matching is used to extract values from grouped data structures. It is also used to branch code paths. The = operator in Elixir is not an assignment operator, rather it is a “matchmaker”. For instance:

{a, b} = {:ok, 42}

In the above code, the variable a gets matched with the atom :ok and b is set to 42

case tp do
{:ok, name} -> <<do something with name>>
{:error, message>> -> <<do something with the message>>
end

In the above code block, if the value of tp is {:ok, "John"} then it will match the first branch in the case block, and the variable name will be assigned the string "John" However, if tp is {:error, :not_found} then it will not match the first branch. The second branch will match, and message will be assigned :not_found

Piping

One of my favourite features of (most) functional languages is the pipe operator, so it gets its own section. The operator takes the value on its left side and passes it as the first argument to the function on its right side. It allows you to switch from writing:

rotate(scale(stretch(square, 1.5), 2), 90)

to:

square |> stretch(1.5) |> scale(2) |> rotate(90)

which is beyond awesome.

Ok — lets get into the thick of things.

The Actual Code

defmodule Roman do
...
end

Elixir is closest to the functional programming paradigm than any other (OOP, procedural, declarative, etc). Related functions, attributes, variables and so on will be grouped under Modules. The declaration above defines a module called Roman and does nothing else.

@values [i: 1, v: 5, x: 10, l: 50, c: 100, d: 500, m: 1000]
@valid_transitions %{
m: [:c, :d, :l, :x, :v, :i],
d: [:c, :l, :x, :v, :i],
c: [:m, :d, :c, :l, :x, :v, :i],
l: [:x, :v, :i],
x: [:c, :l, :x, :v, :i],
v: [:i],
i: [:x, :v, :i]
}

Module attributes such as @values and @valid_transitions above are constants accessible from within the module. Module attributes have other uses, but using them as constants within a module is pretty common. These can be accessed from any functions defined within the module, but are locally scoped, and cannot be accessed from outside the module.

The @values attribute is assigned to a type called a keyword list whereas the @valid_transitions type is a map in which each key (the keys are atoms) has a regular list as its value.


Moving on to the functions:

def to_decimal(roman_string) do
..
end
def to_decimal!(roman_string) do
..
end

The above block defines 2 functions. These can be referred to as Roman.to_decimal/1 and Roman.to_decimal!/1

Functions are scoped to the module in which they are defined and their names will reflect that: Roman. At the end of the string section of the function name (to_decimal ) is its arity.

The arity of a function represents the number of arguments a function takes

This is the /1 in the definitions above. In this case, both expect a single string.

Additionally, one of the functions above has an exclamation mark at the end of the string part of its name to_decimal! This is a convention, rather than a specific requirement. For functions that can return ‘good’ and ‘bad’ values, the output is usually returned as a tuple. The ‘good’ output is returned as the tuple: {:ok, value} whereas the ‘bad’ output would be returned as {:error, message}. Note that these return values are also just a convention, but a rather common one. The output from such a function can be pattern-matched to extract the correct value, or the error message, by the calling function. For instance:

iex> {:ok, decimal_value} = Roman.to_decimal("MCM")
{:ok, 1900}
iex> decimal_value
1900
iex> {:error, error_message} = Roman.to_decimal("XFG")
{:error, "Invalid roman numeral"}
iex> error_message
""Invalid roman numeral"

However, in certain cases, it may be beneficial for the function to raise an Exception rather than return an error tuple. In such cases, a second function which is identical to the first with the one difference being an ! at the end of the string part of its name, could be defined. If the function is successful it would simple return the value required, otherwise it would raise an exception.

iex> decimal_value = Roman.to_decimal!("MCM")
1900
iex> decimal_value = Roman.to_decimal!("XFG")
Error: ...

This type of function pair will often show up in Elixir codebases.

As a final discussion on functions, let’s look at argument destructuring (essentially pattern-matching function arguments) and guards.

defp is_tuple_valid?({from, to}) do
..
end
defp convert({atm, rest}, acc) when atm in [:v, :l, :d, :m] do
..
end
defp convert({atm, rest}, acc) when atm in [:i, :x, :c] do
..
end

The private function (private due to the keyword defp) called Roman.is_tuple_valid?/1 takes a single argument. However, it seems as though it receives 2. In actual fact, the function receives a single two-element tuple as an argument and matches the first element to the variable from and the second to the variable to .

If you needed access to the tuple itself as well (as opposed to just its contents) you could write:

defp is_tuple_valid?({from, to} = tp) do

In this case, in addition to the matched contents, the function also has access to the input tuple itself, in the variable tp The same pattern-matching is used in the subsequent Roman.convert/2 functions that take a tuple as the first parameter, and a number as the second.

Note that the two convert functions have exactly the same definition until the when keyword. This keyword allows for further differentiation between similar functions by changing which function gets called based on some filter applied to the function’s input. This filter is called a Function guard.

Function guards allow you to “match” functions to specific inputs

In the above code instance, if the first value in the tuple (which gets ‘assigned’ to atm) is an atom that is contained in the list [:i, :x, :c] then the second convert/2 function will be called. If it is in the list [:v, :l, :d, :m] then the first convert/2 function is called. Using a guard eliminates the need for to encapsulate the switching logic within the convert/2 function, making it much easier at a glance to identify code paths in a module.


Conclusion

To round it up, what we have gone through above is a tiny bit scraped off the surface of Elixir. The point wasn’t a structured introduction to the language but rather a simple retrospective of little tidbits I came across while putting together the converter script. The code itself within the funtions is mostly self explanatory, even with the slightly esoteric Elixir syntax sprinkled here and there. As my journey through the language progresses, I hope to add some more substance to the various cool bits that I come across.

Until then, thanks for reading and happy coding!