Indeed, you can notice a common pattern for the shape of such functions like List.map_result
that work with different types. And this pattern is usually called traverse
:
val traverse : ('a -> 'b option) -> 'a list -> ('b list) option
val traverse : ('a -> ('b, 'e) result) -> 'a list -> ('b list, 'e) result
val traverse : ('a -> 'b Lwt.t) -> 'a list -> ('b list) Lwt.t
val traverse : ('a -> 'b option) -> 'a array-> ('b array) option
val traverse : ('a -> ('b, 'e) result) -> 'a array-> ('b array, 'e) result
val traverse : ('a -> 'b Lwt.t) -> 'a array-> ('b array) Lwt.t
(* and so on ... *)
You can see that implementing every single monomorphic function will result in a total of O(N x M) implementations which is quite a lot of boilerplate to write and maintain if you ask me.
Good news! It’s possible to implement a single implementation of traverse
per a collection type if the function result type implements the following module signature known as Applicative:
module type Applicative = sig
type 'a t
val pure : 'a -> 'a t
val both : 'a t -> 'b t -> ('a, 'b) t
end
(and types like option
, result
, Lwt.t
and many others can trivially implement this).
Once you have this, you can generalise traverse
, and its implementation will give you map_result
and fold_result
for free. So in some sense, it’s a nice modular abstraction that gives you quite a lot of flexibility.
As for the question about collecting errors, this is already happen to be solved as mentioned by @xvw. You can have the following type usually known as validation
:
type ('a, 'e) validation =
| Failure of 'e
| Success of 'a
And the Applicative
implementation for validation
combines errors instead of returning the first one. Thus, traverse
over a list collects all errors instead of short-circuiting on the first one. I think it’s a nice design because you don’t have the make a choice for an end user (users usually don’t like this), and you give more flexibility while allowing for extensibility.
However, whether it’ll be convenient enough to work with these Applicative
and Traverse
modules instead of monomorphic functions is a separate question