The Problem

The Elm Maybe module only defines Maybe.map, Maybe.map2, …, Maybe.map5. There is no Maybe.map6. Similarly, the Elm Result module defines Result.map, Result.map2, …, Result.map5, and there is no Result.map6.

In Running Out of Maps, Joël Quenneville gives a simple example why functions such as Maybe.map6 or Result.map6 are useful. He starts with a User record that is defined as follows:

type alias User =
  { name : String
  , age : Int
  , address : String
  , email : String
  , role : Role
  }

type Role = Admin | Regular

Defining the User record automatically defines a User record constructor function that returns a User record when passing in values corresponding to the different fields of the User record. For example,

User "Alice" 42 "41 Winter Street" "alice@example.com" Regular

evaluates to

{ address = "41 Winter Street"
, age = 42
, email = "alice@example.com"
, name = "Alice"
, role = Regular
}

Since records with six or more fields are quite common, (record constructor) functions with six or more arguments are quite common as well.

Maybe.map5 can be used to create a User record wrapped in Just if all the required data is present or Nothing if some piece of data is missing.

Maybe.map5 User
  (Just "Alice")
  (Just 42)
  (Just "41 Winter Street")
  (Just "alice@example.com")
  (Just Regular)

evaluates to

Just
  { address = "41 Winter Street"
  , age = 42
  , email = "alice@example.com"
  , name = "Alice"
  , role = Regular
  }

while

Maybe.map5 User
  Nothing
  (Just 42)
  Nothing
  (Just "alice@example.com")
  (Just Regular)

evaluates to

Nothing

Similarly,

Result.map5 User
  (Ok "Alice")
  (Ok 42)
  (Ok "41 Winter Street")
  (Ok "alice@example.com")
  (Ok Regular)

evaluates to

Ok
  { address = "41 Winter Street"
  , age = 42
  , email = "alice@example.com"
  , name = "Alice"
  , role = Regular
  }

while

Result.map5 User
  (Err "Name not found")
  (Ok 42)
  (Err "Address not found")
  (Ok "alice@example.com")
  (Ok Regular)

evaluates to

Err ("Name not found")

Note that the result is Err ("Name not found") and not Err ("Address not found"). Result.map5 returns the first Err value it encounters.

The Solution for the Maybe Case

If we extend the User record (e.g. by adding a field for storing language preferences), we will need something to play the role of the missing Maybe.map6 and Result.map6 functions.

In Running Out of Maps, Joël Quenneville solves this problem for the Maybe case. He defines

andMap : Maybe a -> Maybe (a -> b) -> Maybe b
andMap =
    Maybe.map2 (|>)

and then shows that for any function fn of arity 6

Maybe.map6 fn x1 x2 x3 x4 x5 x6

is the same as

Just fn
    |> andMap x1
    |> andMap x2
    |> andMap x3
    |> andMap x4
    |> andMap x5
    |> andMap x6

The andMap pipeline needs a bit more code but it is still easy read and write.

Before we look at the Result case, let’s try to get a better understanding of what andMap does. The first argument passed into andMap is either Nothing or Just v where v is some value of type a.The second argument passed into andMap is either Nothing or Just fn where fn is a function that takes an a and returns a b. Let’s see what andMap does in detail.

andMap (Just v) (Just fn)
    == Maybe.map2 (|>) (Just v) (Just fn)
    == Just ((|>) v fn)
    == Just (fn v)

andMap Nothing (Just fn) == Maybe.map2 (|>) Nothing (Just fn) == Nothing

andMap (Just v) Nothing == Maybe.map2 (|>) (Just v) Nothing == Nothing

andMap Nothing Nothing == Maybe.map2 (|>) Nothing Nothing == Nothing

If any of the arguments passed in is Nothing, then andMap immediately returns Nothing because Maybe.map2 returns Nothing if any of the arguments passed in is Nothing.

If both the function and the value “are present”, andMap unwraps both arguments, applies the function to the value, and wraps the result in Just.

Elm allows the construction of types that seem to lack an obvious interpretration. In the type signature of andMap we see a Maybe (a-> b) type. What does it mean to work with functions that may not be present? This is hard to understand for people coming from a more object oriented or imperative background. Nevertheless, It is easy to write down concrete examples for such types. According to he Elm REPL, the type of Just (\n -> n + 1) is Maybe (number -> number). The Elm REPL has not problem with these type because it does not care about their meaning and because it treats functions just like it treats any other value.

A First Look at the Result Case

Following Running Out of Maps, we define a function

andMapR_ : Result x a -> Result x (a -> b) -> Result x b
andMapR_ =
    Result.map2 (|>)

The capital R in andMapR_ indicates that andMapR_ is defined for the result case. We will later define andMapR (without the underscore) in a different way.

We can now use the Elm REPL to see that

Ok User
    |> andMapR_ (Err "Name not found")
    |> andMapR_ (Ok 42)
    |> andMapR_ (Err "Address not found")
    |> andMapR_ (Ok "alice@example.com")
    |> andMapR_ (Ok Regular)

evaluates to

Err ("Address not found")

This is somewhat unexpected since

Result.map5 User
    (Err "Name not found")
    (Ok 42)
    (Err "Address not found")
    (Ok "alice@example.com")
    (Ok Regular)

evaluates to

Err ("Name not found")

Simply copying the the construction of andMap from Running Out of Maps does not work for Result values. The andMapR_ pipeline returns Err ("Address not found") instead of the expected Err ("Name not found"). Maybe we can fix this by defining another function that plays the role of andMap when dealing with Result values.

The Result.map2 Case

Let’s look at the simplest Result case. We can verify (e.g. using the Elm REPL) that

andMapR_ : Result x a -> Result x (a -> b) -> Result x b
andMapR_ =
    Result.map2 (|>)

is not working as expected.

Result.map2 fn2 (Err e1) (Err e2)

is not the same as

Ok fn2
    |> andMapR_ (Err e1)
    |> andMapR_ (Err e2)

The Result.map2 call returns Err e1 while the andMapR_ pipeline returns Err e2. But why is this?

To get a better understanding of where things break, we need a better understanding of what andMapR does. Similar to the andMap case for Maybe, we have the following identities:

andMapR_ (Ok v) (Ok fn)
    == Result.map2 (|>) (Ok v) (Ok fn)
    == Ok ((|>) v fn)
    == Ok (fn v)

andMapR_ (Err e) (Ok fn) == Result.map2 (|>) (Err e) (Ok fn) == Err e

andMapR_ (Ok v) (Err e) == Result.map2 (|>) (Ok v) (Err e) == Err e

andMapR_ (Err e1) (Err e2) == Result.map2 (|>) (Err e1) (Err e2) == Err e1

If any of the values passed in is an Err value, then andMap_ immediately returns some Err value because Result.map2 returns an Err value. The last case shown above is the interesting one. If both arguments passed in are Err values, then andMapR_ returns the the first one.

If both the function and the value are Ok values, then andMapR_ unwraps both arguments, applies the function to the value, and wraps the result in Ok.

We have seen that In the andMap case, we have

andMap Nothing Nothing == Maybe.map2 (|>) Nothing Nothing == Nothing

while in the andMapR_ case, we have

andMapR_ (Err e1) (Err e2) == Result.map2 (|>) (Err e1) (Err e2) == Err e1

In the andMap case, we simply return Nothing. There is no first and second Nothing. All Nothings are equal. In the andMapR_ case, order matters. andMapR_ returns the first Err value it encounters.

Now let’s look at what happens with in an andMapR_ pipeline if we pass in two Err values. We start by rewriting the pipeline in one line.

Ok fn |> andMapR_ (Err e1) |> andMapR_ (Err e2)
    == (Ok fn |> andMapR_ (Err e1)) |> andMapR_ (Err e2)  -- add parentheses
    == (andMapR_ (Err e1) (Ok fn)) |> andMapR_ (Err e2)   -- apply pipe operator
    == Err e1 |> andMapR_ (Err e2)                        -- see above_
    == andMapR_ (Err e2) (Err e1)                         -- apply pipe operator
    == Err e2                                             -- see above

It looks like the problem shows up in the last step where we apply andMapR_ to two Err values. andMapR_ returns the first Err value passed in. Let’s construct something that returns the second Err value instead.

The following implementation of andMapR behaves identically to andMapR_ except for the case where two Err values are passed in. In this case, andMapR returns the second Err value instead of the first one.

andMapR : Result x a -> Result x (a -> b) -> Result x b
andMapR ra rb =
    case ( ra, rb ) of
        ( Ok v, Ok fn ) ->
            Ok (fn v)

        ( Err e, Ok _ ) ->
            Err e

        ( Ok _, Err e ) ->
            Err e

        ( Err _, Err e ) ->
            Err e

Now we have to check that a pipeline of two andMapR calls is actually the same as Result.map2. In the transformations below, fn2 always denotes a function of two arguments (with the matching type signature).

Let’s first get rid of the pipe operator so that it is easier to determine the values of andMapR function application

Ok fn2 |> andMapR x1 |> andMapR x2
    == andMapR x1 (Ok fn2) |> andMapR x2            -- apply pipe operator
    == andMapR x2 (andMapR x1 (Ok fn2))             -- apply pipe operator

Case 1: x1 = Ok v1, x2 = Ok v2

andMapR x2 (andMapR x1 (Ok fn2))
    == andMapR (Ok v2) (andMapR (Ok v1) (Ok fn2))
    == andMapR (Ok v2) (Ok (fn2 v1))                -- definition of andMapR
    == Ok ((fn2 v1) v2)                             -- definition of andMapR
    == Ok (fn2 v1 v2)                               -- partial application
    == Result.map2 fn2 (Ok v1) (Ok v2)              -- definition of Result.map2

Case 2: x1 = Ok v1, x2 = Err e2

andMapR x2 (andMapR x1 (Ok fn2))
    == andMapR (Err e2) (andMapR (Ok v1) (Ok fn2))
    == andMapR (Err e2) (Ok (fn2 v1))               -- definition of andMapR
    == Err e2                                       -- definition of andMapR
    == Result.map2 fn2 (Ok v1) (Err e2)             -- definition of Result.map2

Case 3: x1 = Err e1, x2 = Ok v2

andMapR x2 (andMapR x1 (Ok fn2))
    == andMapR (Ok v2) (andMapR (Err e1) (Ok fn2))
    == andMapR (Ok v2) (Err e1)                     -- definition of andMapR
    == Err e1                                       -- definition of andMapR
    == Result.map2 fn2 (Err e1) (Ok v2)             -- definition of Result.map2

Case 4: x1 = Err e1, x2 = Err e2

andMapR x2 (andMapR x1 (Ok fn2))
    == andMapR (Err e2) (andMapR (Err e1) (Ok fn2))
    == andMapR (Err e2) (Err e1)                    -- definition of andMapR
    == Err e1                                       -- definition of andMapR
    == Result.map2 fn2 (Err e1) (Err e2)            -- definition of Result.map2

We see that with the new andMapR function, a pipeline of two andMapR calls is identical to Result.map2. In particular, just like Result.map2, the pipeline of two andMapR calls returns the first Err it encounters.

Generalization to More Arguments and Longer Pipelines

This section essentially repeats the computations of the previous section in an attempt to prove the general case using induction. It does not provide any new insights. This article is not a peer-reviewed academic paper. If you find any error or typo, please let me know.

Subscripts are hard to write in markdown code blocks. Therefore, we are going to change notation a bit. We will be writing (e.g.) f2 as f<2> where the integer in angle brackets plays the role of a subscript. Using this notation, we have proven in the last section that

Ok fn<2>
    |> andMapR x<1>
    |> andMapR x<2>

is the same as

Result.map2 fn<2> x<1> x<2>

Let’s assume that for any integer k, we have

Ok fn<k>
    |> andMapR x<1>
    |> andMapR x<2>
    ...
    |> andMapR x<k>

is the same as

Result.map<k> fn<k> x<1> x<2> ... x<k>

By applying the first k andMapR pipeline steps, we see that

Ok fn<k+1>
    |> andMapR x<1>
    |> andMapR x<2>
    ...
    |> andMapR x<k>
    |> andMapR x<k+1>

is the same as

(Result.map<k> fn<k+1> x<1> x<2> ... x<k>) |> andMapR x<k+1>             (1)

It remains to show that the latter expression is the same as

Result.map<k+1> fn<k+1> x<1> x<2> ... x<k> x<k+1>

Note that in expression (1) we are passing the function fn<k+1> as the first argument to Result.map<k> and we are also providing only k arguments x<1>, x<2>, …, x<k>. This means that expression (1) actually returns a function of one argument wrapped in a Result. This is exactly what the andMapR function expects as its second argument.

Let’s start by applying the pipeline operator in expression (1). We see that (1) is the same as

andMapR x<k+1> (Result.map<k> fn<k+1> x<1> x<2> ... x<k>)

Again, we consider four cases.

Case 1: x<k+1> = Ok v1, (Result.map<k> fn<k+1> x<1> x<2> ... x<k>) = Ok fn

andMapR x<k+1> (Result.map<k> fn<k+1> x<1> x<2> ... x<k>)
    == andMapR (Ok v1) (Ok fn)
    == Ok (fn v1)
    == Ok ((fn<k+1> x<1> x<2> ... x<k>) x<k+1>)
    == Ok (fn<k+1> x<1> x<2> ... x<k> x<k+1>)
    == Result.map<k+1> fn<k+1> x<1> x<2> ... x<k> x<k+1>

Case 2: x<k+1> = Ok v1, (Result.map<k> fn<k+1> x<1> x<2> ... x<k>) = Err e

andMapR x<k+1> (Result.map<k> fn<k+1> x<1> x<2> ... x<k>)
    == Err e
    == Result.map<k+1> fn<k+1> x<1> x<2> ... x<k> x<k+1>

Since, by construction, Err e is the first error passed passed into

Result.map<k> fn<k+1> x<1> x<2> ... x<k>

it is also the first error passed into

Result.map<k+1> fn<k+1> x<1> x<2> ... x<k> x<k+1>

Case 3: x<k+1> = Err e, (Result.map<k> fn<k+1> x<1> x<2> ... x<k>) = Ok fn

andMapR x<k+1> (Result.map<k> fn<k+1> x<1> x<2> ... x<k>)
    == Err e
    == Result.map<k+1> fn<k+1> x<1> x<2> ... x<k> x<k+1>

Again, we see that x<k+1> = Err e is the first error passed into

Result.map<k+1> fn<k+1> x<1> x<2> ... x<k> x<k+1>

because x<1>, x<2>, …, x<k> are all Ok values by construction.

Case 4: x<k+1> = Err e1, (Result.map<k> fn<k+1> x<1> x<2> ... x<k>) = Err e2

andMapR x<k+1> (Result.map<k> fn<k+1> x<1> x<2> ... x<k>)
    == Err e2
    == Result.map<k+1> fn<k+1> x<1> x<2> ... x<k> x<k+1>

Here, Err e2 is the first error value passed into

Result.map<k> fn<k+1> x<1> x<2> ... x<k>

and we show that it is also the first Err value passed into

Result.map<k+1> fn<k+1> x<1> x<2> ... x<k> x<k+1>

The Implementation of andMap in Result.Extra

Interestingly, the Result.Extra Elm module also contains an implementation of andMap that looks as follows:

andMap : Result e a -> Result e (a -> b) -> Result e b
andMap ra rb =
    case ( ra, rb ) of
        ( _, Err x ) ->
            Err x

        ( o, Ok fn ) ->
            Result.map fn o

This is different from the implementation of andMapR above (mainly because it uses Result.map) but thinking through all the possible cases of ra and rb shows Result.Extra.andMap does the same as andMapR.