On Elixir and Static Typing

In a recent discussion I had around development topics in general, static typing came up, and it got me thinking about why I've never missed that, or even thought much about it, while working with Elixir (or Erlang for that matter)..

UPDATE 2015-08-01: I've clarified and expanded the "Pattern matching" section, and added references to some recommended reading at the end..

Why static typing?

One of the primary reasons for a language to have static typing, of course, is that it allows you to catch some bugs at compile time instead of runtime. These bugs are often of a trivial kind that you would quickly spot when trying to run your program. Sometimes, however, it can be a more insidious error, allowing a piece of data to pass through parts of your program that never expected it, causing a lot of grief until you find the source.

Another reason is that it preserves information. To quote Mark Chu-Carrol from his blog post Static Typing: Give it a break!:

When I'm programming, I know what kind of thing I expect in a parameter, and I know what I expect to return. When I'm programming in a weakly typed language, I find that I'm constantly throwing information away, because I can't actually say what I know about the program.

More trivially, but also important to many, static typing is claimed to allow more intelligent IDEs, better refactoring possibilities, etc. I won't dwell more on this as I've never used any advanced Java IDE or similar, but I've previously been pretty comfortable with Vim + a few plugins, and more recently I find that the excellent Alchemist tooling integration into Emacs covers my needs nicely so far.

So why don't I worry about it?

First and foremost, testing. While it didn't play a big role in my early days of programming, I learned to love having unit tests as I was working through a couple of books on Test Driven Development. I sometimes write tests before the code, sometimes afterwards, but I find great comfort in having tests when making changes in existing code, and for that matter verifying that new features work as expected.

It is true that type errors can be missed if you don't have 100% test coverage, but I'd argue that most programming errors are more complicated, and more subtle, than just trying to pass a value of the wrong type. Such errors often won't be caught at all by static type checking.

Secondly, when it comes to expressing intent, in Elixir we have pattern matching, guards and typespecs to help us do precisely that, more fully than the regular type systems of languages like Java or C/C++.

Pattern matching

Something that's at the core of Elixir is pattern matching. It's used for binding variables to values, for verifying the results of operations, and for matching function calls with the correct function clause.

Binding Variables

The = operator in Elixir is actually a match operator, not an assignment operator; it's more of a side effect that it also binds variables on the left-hand side to values, in order to see if the expression can be matched.

In simple cases it behaves much like you'd expect, coming from a language without pattern matching:

iex(1)> x = 1
1
iex(2)> x = 2
2

As can be seen above, by default Elixir will allow rebinding of a variable (as opposed to Erlang, where rebinding isn't possible). This doesn't imply mutability; the name x simply refers to a different value after being bound to 2, the original value wasn't destroyed.

However, you can also use the pin operator (^) if you want to do strict matching, without (re)binding a variable:

iex(3)> x = 1 
1
iex(4)> ^x = 2
** (MatchError) no match of right hand side value: 2

The nature of pattern matching becomes more apparent if you try something more complex:

iex(4)> {x, x} = {1, 2}
** (MatchError) no match of right hand side value: {1, 2}

The above will fail; the match operator can't match up a tuple of two identical values (eg. {x, x}) with a tuple of two different values ({1, 2}), as it won't rebind x again within the same expression.

To make it succeed, we have to make it possible for the = operator to match the shape and content of the tuples:

iex(4)> {x, x} = {3, 3}
{3, 3}

After evaluating this, x will have the value 3, which is what needs to happen for a match.

See the Pattern matching section in the Elixir Getting Started guide for more details.

"Offensive Programming"

Pattern matching is often used to make sure that an operation succeeded, through its return value:

{:ok, conn} = MyClient.connect("some_server.mydomain.net")

In the above code, a failed connection would by convention return something like {:error, :connection_timeout}, which can't be made to match the left-hand side, and so would crash the process instead.

This is a key part of the "fail fast" approach to programming that's encouraged in Elixir & Erlang applications; it's fine if the process crashes, because it'll have a supervisor that restarts it in a known good state, and the error won't have time to spread to other parts of the system.

Function Clauses

When you define a function in Elixir, it can have multiple clauses. Each will be tried in turn, using pattern matching to see if it matches with the function call. This means that instead of just being sure that your parameters are of a particular data type, you can get something much more specific:

def find_in(_, []), do: false
def find_in(value, [value|_]), do: value
def find_in(value, [_|t]), do: find_in(value, t)

The function defined above will search for a given value (first argumen) in a list (second argument).

  • If the list matches [] as in the first clause, it's empty, so we can return false; we don't care about the first argument in this case.
  • In the second clause, if the head of the list matches value, we found the value we wanted; return it.
  • Finally, in the third clause, we know it's not an empty list, and the head didn't match the value; bind the tail of the list to t and recursively call find_in/2 to see if the value is somewhere in the tail.

Note that `` is just a way to tell the compiler that we don't care about the value found in that position.._

Much more complex patterns can be matched, for example binary patterns, structs, nested maps and lists, etc.

Guards

Function clause definitions in Elixir support guards, which can be used to check that the provided arguments abide by certain rules. Together with pattern matching and multiple clauses, this allows for a fairly expressive approach as shown in the simple example below, taken from the Modules section in the Elixir Getting Started guide:

defmodule Math do
  def zero?(0) do
    true
  end

  def zero?(x) when is_number(x) do
    false
  end
end

Interacting with the function would then give the following results:

Math.zero?(0)  #=> true
Math.zero?(1)  #=> false

Math.zero?([1,2,3])
#=> ** (FunctionClauseError)
Typespecs

While types aren't directly expressed in Elixir code, static analysis tools like Dialyzer can do type inference and more.

Even without additional help it'll give useful results, but this can be further improved by using typespecs, the Elixir way of declaring typed function signatures (and custom data types if needed).

As an example, see the @spec lines in the following code taken from the Typespecs and behaviours section of the Elixir Getting Started guide:

defmodule LousyCalculator do
  @spec add(number, number) :: {number, String.t}
  def add(x, y), do: {x + y, "You need a calculator to do that?!"}

  @spec multiply(number, number) :: {number, String.t}
  def multiply(x, y), do: {x * y, "Jeez, come on!"}
end

The specs above simply say that the functions take two numbers as arguments, and return a tuple with a number and a string.

If some polite programmer were to sneak in a change that removed the insults in the above code, like so..

defmodule LousyCalculator do
  @spec add(number, number) :: {number, String.t}
  def add(x, y), do: {x + y}

  @spec multiply(number, number) :: {number, String.t}
  def multiply(x, y), do: {x * y}
end

..then running dialyzer on the compiled code would point out the problem for us:

Invalid type specification for function 'Elixir.LousyCalculator':add/2. The success typing is (number(),number()) -> {number()}
Invalid type specification for function 'Elixir.LousyCalculator':multiply/2. The success typing is (number(),number()) -> {number()}

Conclusion

While static typing can be one of many useful tools to prevent errors and document aspects of your code, I'm rather happy with what Elixir provides in that regard. Especially for preserving information, it's about as expressive as it goes, since you can declare your own custom types that can then even be re-used in other modules.

I'm definitely aiming to be more aware of these issues though, and to improve my code by writing more guards, more typespecs, and running dialyzer more often of course!

Recommended Reading: