Hopp til hovedinnhold

Have you ever needed to combine different Maybe-values to produce another value? In this article, we explore just that.

Let us say that we are given the task to implement a function that combines two values to some other type if, and only if, both values are present. For example, consider the following toContactPerson function:

type alias ContactPerson =
    { firstName: String
    , lastName: String
    }

toContactPerson : Maybe String -> Maybe String -> Maybe ContactPerson
toContactPerson firstName lastName =
    ...

This function is trivial to implement, at least if you are somewhat familiar with the Maybe type. With standard pattern-matching, toContactPerson can be implemented like this:

toContactPerson : Maybe String -> Maybe String -> Maybe ContactPerson
toContactPerson maybeFirstName maybeLastName =
    case ( maybeFirstName, maybeLastName ) of
        ( Just firstName, Just lastName ) ->
            Just (ContactPerson firstName lastName)

        _ ->
            Nothing

Simple enough, but you may have already noticed that this implementation is somewhat strenuous to read. Now, imagine how it would read if we expanded the ContactPerson record with additional fields such as address, phone number, email, etc. Not very pleasant!

Another, arguably much more readable, approach is to use the utility functions Maybe.map2, Maybe.map3, Maybe.map4, and so on. While Maybe.map takes a function a -> b and a Maybe a, Maybe.map2 takes a function a -> b -> c and two Maybes, Maybe a and Maybe b. The mapping function is applied if, and only if, both values are present. With Maybe.map2, our toContactPerson function can be improved to:

toContactPerson : Maybe String -> Maybe String -> Maybe ContactPerson
toContactPerson maybeFirstName maybeLastName =
    Maybe.map2 ContactPerson maybeFirstName maybeLastName

or if we exploit partial application:

toContactPerson : Maybe String -> Maybe String -> Maybe ContactPerson
toContactPerson =
    Maybe.map2 ContactPerson

Expanding ContactPerson further with more properties is also trivial:

toContactPerson : Maybe String -> Maybe String -> Maybe String -> Maybe ContactPerson
toContactPerson =
    Maybe.map3 ContactPerson

toContactPerson : Maybe String -> Maybe String -> Maybe String -> Maybe String -> Maybe ContactPerson
toContactPerson =
    Maybe.map4 ContactPerson

toContactPerson : Maybe String -> Maybe String -> Maybe String -> Maybe String -> Maybe String -> Maybe ContactPerson
toContactPerson =
    Maybe.map5 ContactPerson

Notice that there is no Maybe.map6 or above in the standard library of Elm. Consequently, if we shall ever need them, we have to get them elsewhere. We can either implement them ourselves or just use the Maybe.mapN-capabilities of the Maybe.Extra-library. Although, the need for Maybe.map6 and above can often be avoided by handling the Maybes at an earlier stage. If we, for example, are dealing with a form with multiple steps, we may be able to extract some of the values from their Maybe-wrapper before continuing to the next step in the form. At least the required fields, that is. In other words, it is often a good idea to check if the Maybes can be handled at an earlier stage before reaching for these functions.

Relevant resources recommended by the author

Did you like the post?

Feel free to share it with friends and colleagues