A minor variation on Result.map2. Which FP concept am I discovering? :)

I’m playing around with validating text-entry (such as in a web form) and using this as a chance to practise some basic refactoring in Elm. In the process, I’d like to accumulate the errors from a collection of Results. This has led me eventually to write this function, which I assume has some standard name that I don’t yet know:

resultMap2ButCollectingErrs : (a -> b -> c) -> Result x a -> Result x b -> Result (List x) c
resultMap2ButCollectingErrs f ra rb =
    case ( ra, rb ) of
        ( Err ea, Err eb ) ->
            Err <| [ ea, eb ]

        ( Err ea, Ok b ) ->
            Err <| [ ea ]

        ( Ok a, Err eb ) ->
            Err <| [ eb ]

        ( Ok a, Ok b ) ->
            Ok <| f a b

I searched on Pursuit (thinking that there might be a standard function matching this signature) and didn’t find anything that helped me understand what I’m rebuilding here. (Either that doesn’t exist or I don’t know the abstraction that would help me recognize it.)

What am I rebuilding here? Does this function have a standard name? I looked through the various result-extra libraries and nothing jumped out.

Which FP concept am I rediscovering here? Some monad that’s not quite Result? Something else?

Thanks.

1 Like

liftM2 from Haskell?
https://hoogle.haskell.org/?q=liftM2

I don’t think it has a standard name, but I use that kind of structure in the compiler for canonicalization.

For canonicalization, I want to show errors for all names that have typos, so the errors are all collected. Perhaps interestingly, I believe my version of this structure does not follow “the laws” in that liftA2 can collect more errors than liftM2 if there’s an error in the first argument. (I.e. implementing things with andThen makes it impossible to collect errors from the callback.) In this particular case, I think it’s sensible to get as many errors as possible though!

Anyway, point is, I think there’s no standard name. I just have my own definition of a Result type in the compiler.

P.S. The structure I use for collecting errors is a non-empty “bag” with O(1) appends, that way each map2 is always O(1) and then I flatten the bag at the end in O(n). type Errors x = One x | More (Errors x) (Errors x)

3 Likes

I believe this touches on the subject of applicative functors, but with a twist since we accumulate errors instead.

I first bumped into this pattern while researching this (niche) OCaml library: preface/guides/error_handling.md at master · xvw/preface · GitHub

I can see a few resources/packages calling this pattern “parallel validation” or simply “validation” (regardless of the language)

Here’s an Elm example: GitHub - joneshf/elm-validation: Applicative validation in elm

Your function definition is pretty much identical :slight_smile:

You might find reading the the documentation for the Haskell package validation-selective interesting. It discusses some of the patterns you have “rediscovered” I think.

For accumulating multiple errors, you might find this package helpful: ResultME - elm-error-handling 2.2.1

1 Like

As indicated by others, this is totally “a thing”. The ResultME library linked by @rupert makes a great practical choice of making the accumulating error type a NonEmpty List which is what you are going to want 99% of the time.

You asked about concepts so I’d like to try to expand on that a little.

Generality

First, is whether there is generalization that we can make. Yes. We can combine errors for any type that supports combining/appending. That is a Semigroup which we can represent with a simple function. Please excuse the formatting :pray: but below you will find a function that will build a record of functions for combining the error of any result for which we can supply an append function for the error type

forErrorType :
    (err -> err -> err)
    ->
        { andMap : Result err a -> Result err (a -> b) -> Result err b
        , map2 : (a -> b -> c) -> Result err a -> Result err b -> Result err c
        , map3 : (a -> b -> c -> d) -> Result err a -> Result err b -> Result err c -> Result err d
        , map4 : (a -> b -> c -> d -> e) -> Result err a -> Result err b -> Result err c -> Result err d -> Result err e
        , traverse : (a -> Result err b) -> List a -> Result err (List b)
        , sequence : List (Result err a) -> Result err (List a)
        }
forErrorType append=
    let
        andMap aResult aToBResult =
            case ( aResult, aToBResult ) of
                ( Err a, Err aToB ) -> Err <| append a aToB
                ( Err a, _ ) -> Err a
                ( _, Err aToB ) -> Err aToB
                ( Ok a, Ok aToB ) -> Ok <| aToB a

        map2 f a b = Ok f |> andMap a |> andMap b

        sequence = List.foldl (map2 (::)) <| Ok []
    in
    { andMap = andMap
    , map2 = map2
    , map3 = \f a b c -> Ok f |> andMap a |> andMap b |> andMap c
    , map4 = \f a b c d -> Ok f |> andMap a |> andMap b |> andMap c |> andMap d
    , traverse = \f -> List.map f >> sequence
    , sequence = sequence
    }

Once again, 99% of the time you just want a NonEmpty List to accumulate your errors so I am not saying you would want to do this. I am only saying that you can and that it is general.

You might use this, for example, if your error type were a bitwise Int where each error is represented as a specific bit. In that case you could write

bitwiseResult = forErrorType Bitwise.or

-- and now we can call `bitwiseResult.map2` `map3`, etc. etc. to get combining behavior

Categories

Second, you might ask how this “thing” that you have discovered relates to Categories such as Functor and Monad.

If you don’t care then please just disregard!

The long and short is that as soon as you start accumulating errors in your Result your Result ceases to be a law abiding Monad. You can obviously still call andThen on the Result but the Result’s behavior will not follow the laws. Essentially the laws indicate that you should be able to write andMap in terms of andThen. However, when andMap accumulates errors that ceases to be true. andThen for Result short-circuits (stops) on the first error while andMap combines both errors. So they differ.

This really only matters, in my opinion (which is probably ill-informed and wrong :laughing:), in languages with abstraction over Functor and Monad. In those languages you need types to be substitutable and to follow laws in order to avoid surprising behavior. Since Elm supports neither type classes nor scrap-your-type-classes style abstraction (higher kinds + Rank2 type records) nor OCaml-like modules, this is a moot point. In Elm you are pretty much always aware of the concrete semantics of your type so you don’t have to really worry about it as much. There still might be cases for surprising behavior but I suspect they would be very edge case.

Haskell Examples

However, if interested you can see that for the purposes of obeying laws the Haskell page Data.Validation only implements Applicative for Validation and not Monad. That means they offer andMap, map2, map3, etc. but don’t offer andThen.

By contrast, there is another package ValidateT which has fantastic documentation on the subject. The author does make the Validation a monad by arguing for weaker Monad laws (which is kind of an Obi Wan Kenobi “from a certain point of view” style argument).

PureScript

Then there is the PureScript Validation library which does something really beautiful: They allow you to have your :cake: and eat it too!!!

  • They only make the Validation type an instance of abstract Functor and Applicative and not Monad because they don’t want to violate the laws.
  • However, they offer an Elm-like andThen which is the Monad bind function. :tada:

That means that the PureScript validation type is perfectly safe to be used correctly in abstract settings AND you can also use the very helpful (and often necessary, in my experience) andThen function when working with the validation in concrete settings.

So that type is perfectly law abiding in the abstract and useful in the concrete. I think this is probably the best you can do and it gave me goosebumps the first time I saw it. :laughing:

5 Likes

I’ve heard this called Applicative Validation, which fits with John’s answer

Yep. This is explained in details in the Applicatives chapter of “PureScript for Elm developers”.

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.