Oct 26, 2016
Table of contents:
One of the most defining characteristics of Elixir as a programming language is the fact that it is dynamic, and so all types are inferred at runtime.
This is in contrast to static typed languages where the developer must explicitly declare each type upfront.
Dynamic verses static typing is one of those classic arguments that people love to have when it comes to choosing a programming language, and a lot of people passionately advocate for one side or the other.
An interesting thing about Elixir is the fact that, whilst it is a dynamic language, Elixir does have type specifications, which allow you to define function signatures and custom types.
In today’s tutorial we will be looking at using type specifications, declaring function specifications, and defining our own custom types.
The first thing we will look at is defining function specifications. A function specification is basically defining the arguments and the return value.
For example, imagine we had a Bookshelf
module and we need to add a new add/1
function to add a new book to the bookshelf. The function should accept the name of the book as a string, and it should return the atom :ok
if it was successful.
The specification for this function might look like this:
@spec add(String.t()) :: :ok
In this specification we are saying that the add/1
function accepts a string as it’s only argument and it is returning the atom :ok
. The bit after the ::
is the return value of the function.
We might also require a function that counts the existing books in the bookshelf. This function will have no arguments, and it will return an integer as a count of the books. The specification might look like this:
@spec count :: integer
Finally you can also combine multiple types to create compound types. For example, we might have a function that returns all of the books from the bookshelf. This would return a list of strings and so the specification might look like this:
@spec all :: list(String.t())
You can find a full list of the basic types in the Typespecs documentation.
For the majority of the time the basic Elixir types are probably going to be enough for defining your function specifications. However, it is often desirable to define your own types, especially if you wish to represent some specific concept explicitly in code.
Fortunately, Elixir makes it really easy to define our own custom types.
In the previous example, we were using a string when adding a book to the bookshelf. It would be better if we could represent the concept of a book in this application as a custom type:
defmodule Library.Book do
defstruct [:title]
@typedoc """
A custom type that holds the properties of a book
"""
@type t :: %Library.Book{title: String.t()}
end
In this example I’ve created a Library.Book
struct that has a single property of :title
. I’ve then defined a custom type using this struct. I’ve also declared that the :title
property should be a string.
As a quick side note, in last week’s tutorial we looked at writing documentation in Elixir. One thing we didn’t look at was documenting custom types. To document custom types you can use the @typedoc
annotation as I’ve shown in the example above.
We can now update the specs from earlier to use this new custom book type:
@spec add(Library.Book.t()) :: :ok
@spec all :: list(Library.Book.t())
If you have been following along with this tutorial or you have been experimenting with specs and types on your own, you may have noticed that nothing seems to be different when you declare the function specification. Just because you define a function specification, does not change the fact that Elixir will infer the types at runtime.
So if the compiler is just going to ignore your specifications, what’s the point in having them?
There are basically two main benefits to declaring specifications and using custom types.
Firstly, it acts as documentation. It’s much easier to read and understand code if you know what the types are. Even the best named arguments and functions can be ambiguous when you are trying to get to grips with new code. Function specifications and custom types make it explicitly clear as to what is going on, and tools like ExDoc can take advantage of your specifications to show this kind of detail in the documentation that is produced from your code.
Secondly, you can use Dialyzer, which is an Erlang static analysis tool to find discrepancies or possible bugs in your code. In Elixir we can use dialyxir to make it easier to work with Dialyzer. Whilst using Dialyzer does not guarantee that you will find all bugs and errors in your code, you are sure to get some benefit from writing specs and using a static analysis tool.
Dynamic languages have many benefits, and many drawbacks in comparison with static typed languages. In Elixir, the types are inferred at runtime, however, the language does allow you to define function specifications and custom types.
Whilst this won’t prevent type errors, it does make it really easy to write code that is clear and provides a great way to make documentation and reading your code easier.
It also allows you to use static analysis tools such as Dialyzer.
The choice of using function specifications and custom types is up to you, but I’m sure if you start to dive into the Elixir source code or other Elixir open source projects, you will see the benefit of using them.