When working with the error monad (Result.t) I frequently encounter the problem that I want to combine them with list functions like map or filter, yet this is not anticipated by the standard library. Am I missing something obvious, a useful library, or is everyone expected to re-invent this wheel?
The standard library does not provide these functions currently. I suspect that it could be an interseting proposal to bring up upstream. To be concrete, which functions exactly do you have in mind?
Cheers,
Nicolas
For example:
val map : ('a -> ('b, 'c) result) -> 'a list -> ('b list, 'c) result.val filter : ('a -> (bool, 'b) result) -> 'a list -> ('a list, 'b) result
The question is always what happens in the error case. A simple policy is to return the first error.
* `val all_ok: ('a, 'b) result list -> ('a list, 'b) result`
I believe any code that works with Result.t needs variations of this.
Usually it is based on traverse and sequence (preface/lib/preface_make/traversable.ml at master · xvw/preface · GitHub) and we probably want to have it for more than just result (like option)
I am happy to learn about a general principle; Haskell is a good source of inspiration.
Even it is a little bit rough, Preface (GitHub - xvw/preface: Preface is an opinionated library designed to facilitate the handling of recurring functional programming idioms in OCaml.) can gives some insides ![]()
But that often leads to bad experience for end-users (reporting as much as you know in one step is often better) so be careful with the tools you provide programmers with.
The problem for list/result interaction is that the design is not totally obvious, there can be many variations depending on how you want to treat the error cases. Personally this means that I often just recurse over the list with a function as it is more flexible and for the simplest cases I use these combinators.
I don’t agree that providing a simple (even simplistic) choice in a standard library is bad when the alternative is to provide none. Haskell defines map, filter, sequence over monads (so more general than just Result.t) and probably implements a policy that does not fit all use cases but at least it provides some common ground in the ecosystem.
Can’t help you with Lists, but if you are happy using Seq instead you can use seqes package and then you can do two things:
- result traversors for vanilla sequences:
module Seq = struct
include Stdlib.Seq (* everything in the original namespace *)
(* additional submodule so you can `Seq.Res.iter` with a result-returning iterator *)
module Res = Seqes.Standard.Make2(struct
type ('a, 'e) t = ('a, 'e) result
let return x = Ok x
let bind = Result.bind
end)
end
which lets you then write
List.to_seq ["a";"bb";"ccc";"dd";"e";"";"f";"gg"]
|> Seq.R.iter (fun x -> if x = "" then Error x else (Stdlib.print_endline x; Ok ()))
- result sequences (as in, the type of these sequences has some result baked in)
module SeqRes = Seqes.Monadic.Make2(struct
type ('a, 'e) t = ('a, 'e) result
let return x = Ok x
let bind = Result.bind
end)
with which you can do things like
List.to_seq ["a";"bb";"ccc";"dd";"e";"";"f";"gg"]
|> SeqRes.of_seq
|> SeqRes.M.mapi (fun i x -> if x = "" then Error i else Ok (x ^ x ^ x))
|> SeqRes.iter Stdlib.print_endline
note that SeqRes.iter has a vanilla (non-result) iterator as parameter over a result-based sequence, SeqRes.M.mapi has a result-based traversor as a parameter over a result-based sequence. The Seqes functors take care of separating the different styles of traversing into different namespaces.
You could make a similar library for lists. It’d be simpler because the list type doesn’t have an arrow in there (unlike Seq). So you’d just need the Standard functors which make monad-based traversors over the vanilla structure.
Haskell’s policy is short-circuiting, ie what we are calling here ‘just return the first error’: Data.Traversable
>>> traverse id [Right 1, Right 2, Right 3, Right 4, Left 0]
Left 0
I think it’s a reasonable default because the alternative requires a way to combine the error cases, which means the error type needs to be a list or some other type that can be concatenated.
Hypothetically, if we were to add these to the OCaml standard library, it would probably need to be in a new module like eg Stdlib.ListResult.
@lindig, I also need iter, map and fold_left regularly and here is the file I usually use: owi/src/utils/syntax.ml at main · OCamlPro/owi · GitHub
Containers also has the basics: containers 3.13.1 · OCaml Package
Hello.
A while ago, to learn effects, I played with a generic solution to this problem, able to take a function of type ('a -> 'b) -> 'c and to make it into a function of type ('a -> 'b m) -> 'c m for any monad m. Here is my POC:
module type Monad = sig
type 'a t
val return : 'a -> 'a t
(** Monadic bind, with an extra [~on_error] argument, which runs in the error
case. This is necessary to garbage collect continuations. *)
val bind' :
on_error: (unit -> unit) ->
'a t ->
('a -> 'b t) ->
'b t
end
module Make (M : Monad) = struct
let monadise
(type a b c)
(f : (a -> b) -> c)
: (a -> b M.t) -> c M.t
=
let open Effect in
let open Effect.Shallow in
let module E = struct
type _ Effect.t += Yield : a -> b Effect.t
let yield x = perform (Yield x)
exception Done
end in
fun action ->
let rec handler () =
{
retc = M.return;
exnc = raise;
effc = (fun (type b') (e : b' Effect.t) ->
match e with
| E.Yield x -> (* type b' = b at this point *)
Some (fun (k : (b', c) continuation) ->
M.bind'
(action x)
(fun y -> continue_with k y (handler ()))
~on_error: (fun () ->
(* clean up the stack *)
discontinue_with k E.Done {
retc = (fun _ -> assert false);
effc = (fun _ -> assert false);
exnc = (function E.Done -> () | exn -> raise exn)
}
)
)
| _ -> None
);
}
in
continue_with (fiber @@ fun () -> f E.yield) () (handler ())
end
I’m not sure how good the effect code is. Also, ideally, I would rather have a function, morally of type:
val monadise : (module M : Monad) -> (('a -> 'b) -> 'c) -> (('a -> 'b M.t) -> 'c M.t)
but this is obviously not OK and playing with Monad with type 'a t = does not work because parameterised types are not supported. If anyone has an idea, I’m all ears.
An example in the Option monad:
module OptionMonad : Monad with type 'a t = 'a option = struct
type 'a t = 'a option
let return x = Some x
let bind' ~on_error x f = match x with
| None -> on_error (); None
| Some x -> f x
end
module Monadise_option = Make(OptionMonad)
let monadise_option = Monadise_option.monadise
(* some printing helpers *)
let pp_int = Format.pp_print_int
let pp_option pp fmt = function
| None -> Format.fprintf fmt "None"
| Some x -> Format.fprintf fmt "Some %a" pp x
let pp_list pp fmt =
Format.fprintf fmt "[%a]" @@
Format.pp_print_list
~pp_sep: (fun fmt () -> Format.fprintf fmt "; ")
pp
(* a list to play with *)
let l = [1; 2; 3; 4; 5]
(* some iters *)
let list_iter_opt f l = monadise_option (Fun.flip List.iter l) f
let () =
Format.printf "list_iter_opt:";
assert (Some () = list_iter_opt (fun x -> Format.printf " %d" x; Some ()) l);
Format.printf "@."
let () =
Format.printf "list_iter_opt:";
assert (None = list_iter_opt (fun x -> Format.printf " %d" x; if x = 3 then None else Some ()) l);
Format.printf "@."
(* some maps *)
let list_map_opt f l = monadise_option (Fun.flip List.map l) f
let () =
Format.printf "list_map_opt: %a@."
(pp_option @@ pp_list pp_int)
(list_map_opt (fun x -> Some (x + 1)) l)
let () =
Format.printf "list_map_opt: %a@."
(pp_option @@ pp_list pp_int)
(list_map_opt (fun x -> if x = 3 then None else Some (x + 1)) l)
(* some folds *)
let list_fold_opt f x l =
monadise_option
(fun f ->
List.fold_left
(fun a b -> f (a, b))
x
l)
(fun (a, b) -> f a b)
let () =
Format.printf "list_fold_opt: %a@."
(pp_option pp_int)
(list_fold_opt (fun acc x -> Some (acc + x)) 0 l)
let () =
Format.printf "list_fold_opt: %a@."
(pp_option pp_int)
(list_fold_opt (fun acc x -> if x = 3 then None else Some (acc + x)) 0 l)
(* you can even sort *)
let l = [3; 4; 1; 5; 2]
let list_sort_opt f l = monadise_option (fun f -> List.sort (fun x y -> f (x, y)) l) (fun (x, y) -> f x y)
let () =
Format.printf "List.sort: %a@." (pp_list pp_int) (List.sort Int.compare l)
let () =
Format.printf "list_sort_opt: %a@."
(pp_option @@ pp_list pp_int)
(list_sort_opt (fun x y -> Some (Int.compare x y)) l)
Now if that is useful, I will happily clean up the interface (I think it needs helpers to manipulate functions of more arguments and to curry/uncurry on the fly) and put that in a small package on OPAM.
I think there are several good ideas in this thread:
- covering basic List function ( owi/src/utils/syntax.ml at main · OCamlPro/owi · GitHub )
- providing printf-style formatting for error strings ( containers 3.13.1 · OCaml Package )
- taking inspiration from Haskell (sequence, first-error policy)
- Providing wrappers to catch exceptions and turn them into an error value
Personally I struggle with heavily functorised and abstract code like preface/lib/preface_make/traversable.ml at master · xvw/preface · GitHub where I see more module expressions than value types to see how this applies to my error handling problem.
In my experience that’s something you define and then never use :–) Because you forget the combinators exist and doing so with the syntax for exception handlers is natural.
Couldn’t resist: github:niols/ocaml-monadise.