A 4 minute read written by
One of the great things about Elm is the ability to easily and accurately model your data with the use of custom types. Custom types are normally used when a type can have multiple shapes, or represent multiple state. For instance, you can create a custom type to represent the state of a user in your webapp, like this:
type UserState = NotLoggedIn | LoggedIn UserData
In this example, a user is either not logged in, in which case we don't have any data about the user, or the user is logged in, and we have some user data. We call the two "variants" of the custom type (
LoggedIn) the custom type's constructors. Since they are, in fact, functions that return a value of type
Even though custom types usually have at least two constructors, you can actually create custom types with only one constructor. This might seem pointless the first time you hear about it, but it can be quite useful in certain situations. We will examine one use for these single-constructor custom types in this post, while we will look at another (probably more widely used) use for them in a post later in December.
In our use case, we will consider the situation where we have a function for calculating body mass index (BMI) in our app. A person's BMI depends on their weight and height, and can be written like this (using kilos for weight and meters for height):
bodyMassIndex : Float -> Float -> Float bodyMassIndex weight height = weight / (height * height)
Now, while this function works correctly, it's not exactly a pleasant experience to use. Consider, for example, the following code, where we use the
viewBmi : Model -> Html a viewBmu model -> text (bodyMassIndex model.height model.weight)
Did you spot the error? No? Well, neither would the Elm compiler. The error is that our
bodyMassIndex function expects the first argument to be
weight, but we passed
height as the first argument. While the person using our
bodyMassIndex function would probably notice their error quite fast, it is still unnecessary that we allow for the error to be made. This is an example of where we could use single-constructor custom types.
We can start by making two single-constructor custom types:
type Weight = Weight Float type Height = Height Float
Both constructors of the custom types have the same name as the name of the custom type, which is normal to do with single-constructor custom types.
Next, let's say we change our
bodyMassIndex function to have the following type signature:
bodyMassIndex : Weight -> Height -> Float
Now we can use the new version of
bodyMassIndex in the view:
viewBmi : Model -> Html a viewBmu model -> text (bodyMassIndex (Height model.height) (Weight model.weight))
If we did this, we would get a compilation error, saying that
bodyMassIndex expects it's first argument to be a
Weight, not a
Height, which is what we wanted to achieve!
The price of this refactor, however, is the added complexity to our
bodyMassIndex function, since the most basic implementation of the new type signature would look something like this:
bodyMassIndex : Weight -> Height -> Float bodyMassIndex weight height = case weight of Weight weightFloat -> case height of Height heightFloat weightFloat / (heightFloat * heightFloat)
At this point, you are probably thinking that this just isn't worth it. Because that is one ugly function. But don't worry, Elm has a neat trick for making this function almost as simple as the first version, which had only
Floats as arguments.
Similarly to the technique described in yesterday's post, where Jørgen showed that we can pattern match on fields in a record in the argument definition of a function, we can also do this with single-constructor custom types. The name of the constructor and the variable name you want to use is written inside parenthesis in the function definition, which makes the function almost identical to the first version of the function:
bodyMassIndex : Weight -> Height -> Float bodyMassIndex (Weight weight) (Height height) = weight / (height * height)
The overhead of making a refactor like this is now almost nothing, and can, in certain situations give your code guarantees that it otherwise wouldn't have.
Worth noting is that this pattern matching only works with single-constructor custom types, not custom types with multiple constructors. And, as mentioned above, we will see another use for this technique of using single-constructor custom types, later in December.