I found a bug in some code last night that I want to talk about. The problem was that I have two logical types in my Code base ImageId and OwnerId (The code works with data from Instagram, so those represent an image and the person who posted it). The problem that I found was that they were both represented with type String, So somewhere there was some code that should have accepted an OwnerId and instead was being passed an ImageId.

The reason I am kicking myself is that I should have known better. I have been using Erlang for years. While Erlang is regarded as a dynamicly typed language it does have a post compile type checker called Dialyzer as well as pattern matches to prevent you from passing the wrong data to a function.

I have been an advocate of Tuple Types in Erlang. For those who don’t know Erlang a Tuple Type is where you create a tuple of an atom and the actual value. So a UserId might be represented as -type user_id() :: {'user_id', string()}.. For those of you who are not as familiar with Erlang that statement says that there is a type user_id() which is composed of a tuple with the atom ‘user_id’ in the first position and a string in the second. (Actually in Erlang I would use the binary() type not the string() type but that is not important here).

-type user_id()  :: {'user_id', string()}.
-type owner_id() :: {'owner_id', string()}.

The advantage of having types like above is that if you try to pass an owner_id to a function that accepts a user_id it will be rejected. This can be caught by dialyzer as a post compile step or by a pattern match in the function header at run time (or better yet both). It will also tell you what something is if it shows up in a logfile, which is often useful!

As in the Erlang example above, a type like String or Int only tells you what the data is, not what it is used for. You have an integer, great! Is it a count of something (What?), height, a width, and id something else? No way to tell. Because of that there is no way to prevent you from doing things that make no sense. Want to add a height to an ID? Sure the compiler will let you do that.

type BoxHeight = BHeight Int
type BoxWidth  = BWidth Int
type BoxArea   = BArea Int

But what if we created types like this? The compiler will only let the code do things to those types that we explicitly tell it they can. Consider the following function, which computes the area of a box (Width * Height), it takes a BoxWidth and BoxHeight and returns a BoxArea type. Now if there is a type floating around the code base CircleRadius which also contains an int and you try to pass it to this function the type checker well tell us that it is wrong.

getBoxArea : BoxWidth -> BoxHeight -> BoxArea
getBoxArea  (BWidth w) (BHeight h)= BoxArea (w * h)

In use the type checker in the elm compiler to eliminate as many bugs as possible by wrapping raw types in semantic types that fit with the domain of your program.

Added benefit, when you look at tagged types in the debugger it shows the type, so it can remind you of what that piece of data is doing.

If you find this interesting then you should know that I am working on a book on Testing Elm

Buy Testing Elm Now!