17-Dec

Elm

Greater type safety with opaque types!

It may become tedious to always check if a value in your records is on the supposed format. To be absolutely certain that a value correctly represents a property in your records, we can use opaque types!

3 min read

·

By Vetle Bu Solgård

·

December 17, 2020

In all languages, we might want to represent values that is easily representable by simple types like String and Int, these types does not, however, ensure that the desired values are correct. For example, we would expect a year to have at least four digits, or a full name to consist of a first and last name, etc.

type alias Person =
    { name : String
    , birthYear : String
    }

In this record called Person we have two properties, one name and one birthYear. While this type alias can hold a name and a birth year, formatted in the correct way, we don’t have any guarantees that the values in the record actually are in that proper format. The point of this is that we want to be certain that the properties of this record represents the desired values the correct way.

Let's imagine that to create a Person the user needs to fill a form where he/she types in the mentioned properties. If we only checked that the fields were properly formatted when the user submitted the form, but continued to use Strings as the type of the name and birthYear fields, we wouldn’t know for certain that the fields were properly formatted after that check. Maybe we inadvertently changed one of the fields at some time after that, or maybe another person working on the code came along and changed how the check was, not knowing what format the fields needed to be in. To achieve the certainty that the property birthYear is in fact a year, we use opaque types.

module Year exposing (Year, fromString)

type Year =
    Year String

fromString : String -> Maybe Year
fromString yearValue =
    let
        isYear : String -> Bool
        isYear year =
            year
                |> String.toInt
                |> Maybe.andThen
                    (\number ->
                        if number >= 1000 && number < 10000 then
                            Just number

                        else
                            Nothing
                    )
    in
    if isYear yearValue then
        Year yearValue
    else
        Nothing

Year is now a type and the property yearValue is inaccessible from other modules. Notice the way we expose Year, module Year exposing (Year, fromString). As you can see from the way we expose our type, we do not expose any constructor details which we would have done if we wrote ... exposing (Year(..), .... We only expose the fromString function so that we control entirely how the Year type is created. From the definition of the fromString function we can see that we only return the Year type if the String input is indeed a year.

Now we also have a way of validating the "year" input part of the form where the user creates a Person. The property birthYear is now guaranteed to be a year by the type system.

type alias Person =
    { name : String
    , birthYear : Year
    }

The opaque types version do produce some boilerplate code, but will oftentimes be much cleaner to work with as the interaction with the type is clearly defined through getter and setter functions. In a nutshell, opaque types can be used to ensure that we use a module as intended. It will expose everything you need to know on how to interact with it, and also impact your code to become cleaner where the module is used. Unnecessary implementation details will be hidden away and for the cost of some boilerplate code in the module it will be very easy to work with and extend functionality.