Result.t combinators

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?

1 Like

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.

1 Like

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)

1 Like

I am happy to learn about a general principle; Haskell is a good source of inspiration.

1 Like

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 :slight_smile:

1 Like

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.

1 Like

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.

2 Likes

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:

  1. 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 ()))
  1. 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.

1 Like

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.

1 Like

@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

1 Like

Containers also has the basics: containers 3.13.1 · OCaml Package

1 Like

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.

1 Like

I think there are several good ideas in this thread:

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.

1 Like

Couldn’t resist: github:niols/ocaml-monadise.

2 Likes