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 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 ), 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 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.
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.