23-Dec

Elm, Functional

Mapping more than lists

Using the function map to change the elements of a list has become a common pattern in modern programming languages. It is often used in Elm and map is also used in Elm for the Maybe and Result types.

4 min read

·

By Eivind Dagsland Halderaker

·

December 23, 2021

Using the function map to change the elements of a list has become a common pattern in modern programming languages. It is often used in Elm and writing a function to double all the integers in a list in Elm can be written as such

doubleInts list =
    List.map (\number -> number * 2) list

As expected, running the function doubleInts [1,2,3,4] will return [2,4,6,8]. You might wonder what other types map is defined for in Elm and how they operate.

Maybe


In Elm there is a type called Maybe which is used when a function is not guaranteed to return a proper value. A value of Maybe has two variants, it can either be a value, written

Just value

or not a value, written as

Nothing

To give an example, say you have a String and want to convert it into a Float using the function String.toFloat, the String might not represent a valid floating point number. As such, it's type signature is a String as parameter and Maybe Float as it's return type.

toFloat : String -> Maybe Float

If given the String "0.5"

String.toFloat "0.5"

it will return the value

Just 0.5

as "0.5" is a valid floating point number.


On the other hand, if given the string "abc"

String.toFloat "abc"

the conversion to a floating point number fails and as expected the returned value is

Nothing

Both these value still have the type Maybe Float as both are valid attempts of parsing a floating point number.


The Maybe type is common in functional programming languages, such as Haskell's Maybe and Scala with it's Option. It is used often in functional programming instead of throwing exceptions and null-values.


The definition of the type Maybe in Elm is:

type Maybe a
    = Just a
    | Nothing

Mapping a Maybe


When mapping a list, each element is transformed by a given function. In the case of a Maybe, there is either a value or not a value, and can thought of as mapping either an list with a single element or an empty list. You're hoping there's an element inside the Maybe box to run the function on, however, if the Maybe is a Nothing, it will just stay as a Nothing after mapping the function.

How the types change


Even though mapping a function of a Nothing value will not change the value itself, it will still affect the type of the returned Maybe. Say one wants to check if the Float inside is greater than 5 by running

Maybe.map (\float -> float > 5) maybeFloat

If give an actual value

Maybe.map (\float -> float > 5) (Just 0.5)

The returned value still is a Maybe, but it's type has changed from Maybe Float to Maybe Bool.
When given a Nothing

Maybe.map (\float -> float > 5) Nothing

There is nothing to compare to, and as such the returned value will still be

Nothing

and it's type will be Maybe Bool, as we tried to compare the potential floating point value to 5.


The definition of map for a Maybe is

map : (a -> b) -> Maybe a -> Maybe b
map function maybe =
    case maybe of
        Just value ->
            Just (function value)

        Nothing ->
            Nothing

Result


Similar to Maybe, Elm has a type to represent values that either succeeded or gave an error which is called Result. It has two variants:

Ok value

and

Err error

Defined in Elm as

type Result error value
    = Ok value
    | Err error

Given that you want to parse a JSON object as a String, "{ "height": 5}".

parseJSON : String -> Result String Int
parseJSON JsonString = Decode.field "height" Decode.int

and check that the height is higher than 10 we can use map on the Result value

higherThan10 : Result String Int -> Result String Int 
higherThan10 resultHeight = Result.map (\height -> height > 10) resultHeight

Similar to Maybe

Result.map (\height -> height > 10) (Ok 5)

will return

Ok False

and

Result.map (\height -> height > 10) (Err "Failed to decode JSON object")

will return

(Err "Failed to decode JSON object")

Here is the definition of map on a Result

map : (a -> b) -> Result error a -> Result error b
map function resultValue =
    case resultValue of
        Ok value ->
            Ok (function value)

        Err error ->
            Err error

Functors


The way map is defined for different types is inspired by the Functor design pattern in functional programming, which in turn is derived from category theory in maths. In general for a value to be a Functor it has have a function map with a type signature map : (a -> b) -> f a -> f b, where f is the type constructor, i. e. Maybe, Result, or List. If a functor maps the identity function, it has to return the functor unchanged.