We can't find the internet
Attempting to reconnect
Elixir Programming Language
2016-06-26Introduction to Elixir some useful programming language features. This post covers several topics that are useful in general Elixir programming as well as when used in the context of test automation.
This post, the second in the series, shows some language features that can be particularly useful when used in a test automation project. It is mostly written from the point of view of a person who would usually write their tests in Ruby and is familiar with the Ruby testing ecosystem.
Language features
Elixir is a relatively new programming language that runs on the Erlang Virtual Machine (BEAM) bringing with it scalability and fault tolerance.
As the language website describes it:
Elixir is a dynamic, functional language designed for building scalable and maintainable applications.
This post isn’t going to be an introduction to the language per se but rather a list of some of the features that I personally have found useful. The features mentioned here will be later on used to write some of the test examples (as they would in almost any Elixir program).
The next chapter lists some of the excellent resources available for new Elixir developers.
Learning resources
One of the strong points of the Elixir ecosystem is the quality of the documentation. It is obvious when visiting the Elixir language website for the first time and when digging deeper into a specific area, for example, the testing framework ExUnit
There are several good books about the language too. The one that is usually on top of the recommendation list for people new to Elixir is Programming in Elixir by Dave Thomas. The book goes through most language features and is a very good introduction to Elixir.
Another book that I found very useful was Elixir in Action by Sasa Juric. It goes in depth in describing how to build applications using the OTP (Open Telecom Platform) framework. It gives practical advice on creating scalable, concurrent and fault tolerant applications using Elixir. It’s a real eye opener and shows just how powerful BEAM, the Erlang VM is.
Elixir Koans is another good resource that gives you a set of failing tests you need to get passing giving essentially a practical tour of many language features and libraries.
More resources for learning Elixir are listed on Elixir website's learning section
Syntax
Elixir syntax is somewhat similar to Ruby and some of the modules look familiar to someone with a Ruby background. The languages can look deceptively similar and this post by Clark Kampfe explains the differences in much[more detail than I could.
Back to the similarities. Comparing the Elixir String module to the String class in Ruby we can see the following:
String.reverse("text") vs. "text".reverse
String.length("text") vs. "text".length
While the method names are the same, the way to call them is different. In Ruby, being an object oriented language, calling a method is done by sending a message to the object, in this example an object of the String class.
In Elixir, functions are completely separate from data, and are organised in modules instead of classes. Data is passed in to the functions, which transform the data and return a result. Data is immutable although a variable can be rebound to a different value. Parenthesis are optional in Elixir as in Ruby, except when they are used with the pipeline operator. As in Ruby, many Elixir functions also support the ‘!’ version but the meaning is different. While in Ruby the bang operator changes the data in place (for example, a list), in Elixir a function ending with a ’!’ returns an exception instead of a tuple with the function call status. For example, opening a non-existing file in Elixir:
iex> file = File.open("idontexist")
{:error, :enoent}
iex> file = File.open!("idontexist")
** (File.Error) could not open "idontexist": no such file or directory
(elixir) lib/file.ex:1054: File.open!/2
Pattern matching for function calls
Functions in Elixir are defined by the name of the function and also by it’s arity, the number of parameters it expects. An example of this are the functions String.reverse/1
and String.splice/3
in the String module, which take 1 and 3 parameters respectively.
Elixir doesn’t select which function to invoke by it’s arity but also by pattern matching the input parameters. The following functions have the same arity but pattern matching is used to determine which one is used:
defmodule Mathsy do
def fib(0), do: 1
def fib(1), do: 1
def fib(n), do: fib(n - 1) + fib(n - 2)
end
The first function is invoked when the value passed in is 0 and the second one when it’s 1. The last function is used for any other values and calls fib function recursively. But what happens if the value passed is not a number? There is nothing checking that the type of the parameter is correct. We’ll get an error for passing in an invalid value:
(ArithmeticError) bad argument in arithmetic expression
This can be fixed by adding a guard clause to the function definition, which makes sure that the argument passed in is of the expected type.
defmodule Mathsy do
def fib(0), do: 1
def fib(1), do: 1
def fib(n) when is_integer(n), do: fib(n - 1) + fib(n - 2)
end
Now passing in a character doesn’t crash the function anymore. We can still pass in a negative value, though. Let’s fix that with another guard:
defmodule Mathsy do
def fib(0), do: 1
def fib(1), do: 1
def fib(n) when is_integer(n) and n > 0, do: fib(n - 1) + fib(n - 2)
end
After this change, our function doesn’t generate an ArithmeticError but it also doesn’t cover all possible input values. That is sometimes fine and we can just let it the VM handle it but if we want to know what gets passed in, we can add another function head for completeness:
defmodule Mathsy do
def fib(0), do: 1
def fib(1), do: 1
def fib(n) when is_integer(n) and n > 0, do: fib(n - 1) + fib(n - 2)
def fib(other), do: raise ArgumentError, message: "Invalid argument #{other}"
end
In a real application, logging the value is probably what you’d want but here we just raise an error.
It’s important to have the functions defined in the correct order as fib(n)
would also match fib(1)
otherwise. Function definitions can be written in two ways and the one above is typically used for short function bodies. When the function has more lines, it is usually in the longer format
def transform(value) do
value_1 = transform_one(value)
value_2 = transform_two(value_1)
end
or with the Elixir pipeline operator (more of which later)
def transform(value) do
value
|> transform_one
|> transform_two
end
The result of the last statement is returned to the caller and no specific return statement is required.
Pattern matching for assignments
Pattern matching is used not only in function definitions but also when assigning values to variables.
[1, 2, c] = [1, 2, 3]
[1, 2, c] = [2, 4, 6]
After first line above, the value of c
is 3, and the pattern match effectively becomes
[1, 2, 3] = [1, 2, 6]
The second line fails because the list on the left can’t be made to match the one on the right. This can be very powerful when matching structured data, for example HTTP responses in tests.
Structured data
As mentioned above, pattern matching can be used also for more complex data structures. It is a map/hash/dictionary with type information included. A struct is the Elixir way of defining structure to a map. Whereas any keys can be set in a map, a struct only allows for the predefined keys to be set. Included keys and their default values can be defined and a struct can contain other structs. In an example relevant to testing, a popular HTTP client library for Elixir HTTPoison, returns a response in a struct (HTTPoison.Response
) like the following:
{:ok,
%HTTPoison.Response{
body: "<HTML>...\r\n",
headers: [
{"Cache-Control", "private"},
{"Content-Type", "text/html; charset=UTF-8"},
{"Location", "http://www.google.co.uk/?..."},
{"Content-Length", "261"},
{"Date", "Wed, 04 May 2016 21:29:09 GMT"},
{"Age", "0"},
{"Connection", "keep-alive"}
],
status_code: 302
}
}
The response starts with a typical Elixir idiom, i.e. returning the status of the operation as the first item in a tuple, similar to the following example:
{:ok, response}
{:error, reason}
HTTPoison supports the bang operator in which case it just returns the HTTPoison.Response struct without the status. How this relates to pattern matching and also makes it very useful for testing is that pattern matching can be used to match any field in the response, either in a case statement or in a function head. The following shows an example of a case statement using pattern matching against the response struct:
case HTTPoison.get!("http://www.google.com") do
%HTTPoison.Response{status_code: 200} -> IO.puts "OK"
%HTTPoison.Response{status_code: 302} -> IO.puts "Redirected"
_ -> IO.puts "Neither of the above"
end
The next code block shows how pattern matching could be used in function heads. Each result that we want to have specific handling for can be handled in it's own function:
def decode(%HTTPoison.Response{status_code: 200} = response) do
# do something with the successful response
end
def decode(%HTTPoison.Response{status_code: 400} = response) do
# <use the response in case of a 400
end
In this example the local variable response
would be set to the value of the HTTPoison.Response
that was passed in if the status matches the one defined in the function head. Using pattern matching is an effective way for reducing branching and if statements in code.
Pipeline operator |>
In a functional language, the typical thinking is to have data coming in to the system, transforming it in one or more steps and then returning the data. This can quite easily lead to code that needs to be read inside out, i.e. the inner most operation is the one that gets executed first. The pipeline operator allows us to instead write the code in a more natural way:
uppercase_words =
"text and spaces"
|> String.upcase
|> String.split(" ")
When using the pipeline operator, the result of a step is passed to the next step as the first argument. For more complicated transformations, specific functions can be defined for each step as shown in the following example.
Note: Named functions can only be defined inside a module. Also, this is just an example of the technique, it doesn’t really make sense to redefine the same exact operation in a new function.
defmodule Example
# def is for public, defp for private functions
def upcase(string) do
String.upcase(string)
end
def split_words(string) do
String.split(" ")
end
end
uppercase_words =
"text and spaces"
|> Example.upcase
|> Example.split_words
Language tools
Elixir has a built in build tool, mix
. It can be used to setup a new project, to retrieve dependencies, to compile the project or for running tests. mix help
gives the full list of options available.
Another extremely useful tool for anyone working with Elixir is iex
, interactive Elixir, a REPL that can be used to run Elixir code in a standalone mode like irb
or pry
. Running iex
in the context of your project and it’s dependencies can be done with iex -S mix
. When run like this, any code that you’ve written and included in the project is available in the iex
session and can be called directly. iex
also has very good documentation of the language accessible with the h function, for example h String.length
returns the following:
iex
also has autocompletion for function names, which makes it easier to get to know the functions offered in each module. Just typing the name of the module and hitting tab gives you a list of functions defined.
Summary
This was a brief overview of language features what comes to features that one would first encounter when considering using Elixir in a test automation project. In the next topic, we’ll get to actually implement some tests.