For the last few weeks I’ve been doing a deep dive into Elixir/Erlang’s success types (I will be posting more about that soon). During that time I ran into some issues where I was confused about the type signatures I was seeing. As a Rubyist, I found myself reaching for something like this:

"foo".class #=> String

However, because of Elixir’s pattern matching, this isn’t something you should actually be doing in your code. Instead we have guard clauses.

def foo(bar) when is_atom(bar) do
IO.puts(bar)
end

In order to do a quick inspection of the types being passed into a given fuction, I need something that looks like this:

def foo(bar) do
Type.check(bar)
|> IO.puts
end

I built a little module to accomplish this functionality. I thought it would make a good example for this blog post.

My goals for this package are as follows:

Simple API

Well tested

Well documented

For the API, let’s take the above example. I want it to look something like this:

iex> Type.check(:foo)
'Atom'
iex> Type.check(%{hello: 'world'})
'Map'

I will start by creating the package:

$mix new type
$cd type
$mix test

Let’s take a look at the files that mix has generated for us.

First we have the config/config.exs. We won’t be utilitizing this file in this exercise. This is the file you can use to add your own configurations to a package. There is further documentation here if you are interested.

Next we have the lib folder. This is where all of the code for our package will go. Since we don’t have too much functionaility in this library, we will probably only need to use the provided lib/type.ex which has already defined a module for us.

defmodule Type do
end

Then, under test/, we have two files: test/test_helper.exs and test/type_test.exs.

Finally, we have the README and the mix.exs file. The mix file is where we will list our dependencies and other package metadata.

Writing your first tests

We have a pretty good idea what we want our code to do, so let’s start by writing some basic tests.

Out of these, I am least sure what is_number does. From the Elixir docs:

isnumber(term) Returns true if term is either an integer or a floating point number; otherwise returns false

It looks like we don’t need to define this as it is a super type of floats and integers. There is no situation where I would want the Type.check/1 function to return ‘Number’ instead of a ‘Float’ or ‘Integer’. I want the most specific type it can regcognize.

That should cover the rest of the types we want our function to handle.

Making the tests pass

I will start out with a function that takes any argument, and returns a character list representation of the type of the argument.

To simplify the API, I will create a single function as an entry point to the module that calls a private function. I use the typespec notation to show that this function takes any type and always returns a character list.

@spec check(any()) :: char_list
def check(arg), do: _check(arg)

I will implement the rest of the module. We can use the built in guard clauses.

It looks like booleans and nil must be implemented under the hood as atoms. Because Elixir will match in the order the functions are defined, and because defp _check(arg) when is_atom(arg), do: 'Atom' is defined third, it is being called before the checks for booleans and nil are reached. A quick check of the documentation confirms my suspicion: “The booleans true and false are, in fact, atoms.”

So, we move the defp _check(arg) when is_atom(arg), do: 'Atom' to the bottom and rerun the tests. They all pass.