elm christmas

No nulls in Elm

←Previous postNext post →

How to deal with absent values

A 4 min read written by
Ragnhild Aalvik

Elm, as mentioned earlier, is claimed to have "no runtime exceptions". A big reason for this almost-fact is that Elm doesn't allow the use of null. Whenever a value may not be present we are forced to explicitly tell this to the compiler, using the Maybe construct.

What are Maybes

Looking back at the User example from an earlier article we remember that one of the fields had the type Maybe String:

type alias User =
    { loggedIn : Bool
    , username : Maybe String
    , administrator : Bool
    }

As we probably remember this meant that the username String might be there, but only if the user is logged in. If the user isn't logged in we have no username to put into the User record. In that case we may be tempted to just insert null, but we know that that's not allowed. In Elm we would instead insert the value Nothing. This may not sound very helpful, didn't we just replace null with another absent value? Be patient, the cool thing is in how we use Maybes. We will come to that in a second.

The Maybe construct is a custom type which either has value Just value or Nothing:

type Maybe a
    = Just a
    | Nothing

Notice that Maybe is generic over type a, which could be anything (an Int, String, List String...). In the User example above a is a String. Think of Maybe as a wrapper around any value that may or may not be there, which we must explicitly handle in both cases.

Using Maybes

The most common way of handling Maybes is by case expressions:

getHighscoreText : Maybe Int -> String
getHighscoreText maybeScore =
    case maybeScore of
        Just score ->
            "Current highscore: " ++ String.fromInt score

        Nothing ->
            "No highscore yet."

Here the getHighScoreText function takes a highscore wrapped in a Maybe, and should return a meaningful message. We unwrap the highscore value with a case expression, and we are forced to explicitly say what happens if the value is there, and what happens if it is not. The program simply won't compile if you haven't handled both cases. This makes the program more robust since we cannot accidentally have a missing value.

Other ways of dealing with Maybes

Case expressions isn't the only way of dealing with a Maybe. The Maybe module includes some convenient functions. Two of the most useful ones are Maybe.withDefault and Maybe.map. Here is what they look like and how to use them:

Maybe.withDefault : a -> Maybe a -> a

Maybe.withDefault 0 (Just 4)        -- 4
Maybe.withDefault 0 Nothing         -- 0

Maybe.map : (a -> b) -> Maybe a -> Maybe b

Maybe.map (String.fromInt) (Just 4) -- Just "4"
Maybe.map (String.fromInt) Nothing  -- Nothing

Maybe.withDefault is used for unwrapping a Maybe just like case expressions. It returns the value inside the Just or returns the given default value if the value was Nothing.

Maybe.map is used when you want to change the value inside the Maybe (if present), and wrap it inside a Maybe again. The function given as an argument is applied to the value if it is there, and if the value is Nothing then nothing (😆) happens and Nothing is returned.

Library functions returning Maybes

When using Elm you will meet Maybes all the time. Library functions that may fail will always return a Maybe (or a similar construct such as Result). For example the list function List.head, which should return the first element of the list, will return the element in a Maybe since the list may be empty.

Summary

When deciding types for the values in your program try to think if some of them would fit better as Maybes. If for example a variable doesn't have a meaningful initial value, then it might be worth considering a Maybe.

All in all, play around with Ellie and try to get comfortable with Maybes!

←Previous postNext post →