In a recent project, I was needing to “mix” a couple of different monads: concurrency monad (Async.Deferred
), error monad (Core.Or_error.t
), and option monad.
I’m using ppx_let for monadic let bindings, so it isn’t too bad.
However, I haven’t really written too much code using async (or lwt), so I’m not sure whether I could be doing things in a nicer way.
(Note: this post is similar to this one: How to combine 3 monads: Async/Lwt, Error and State? .)
tl; dr
I’m looking for tips/advice/strategies/articles/info on “mixing” monads.
Example
Here is a toy example to show a pattern that sort of popped up again and again in my code. I annotated types and added comments to help make clarify what’s going on.
(* Move from the option monad to the Or_error monad. *)
let or_error_of_option : 'a option -> 'a Or_error.t = function
| None -> Or_error.error_string "None"
| Some v -> Or_error.return v
(** [or'] returns [v] if [t] is [Ok v], otherwise, when [t] is [Error e],
returns the result of [default e].
I.e., for logging errors, then providing default values.
{[
let a = Or_error.error_string "Something bad!"
let b =
or_default a ~default:(fun e ->
(* Log the error *)
prerr_endline @@ Error.to_string_hum e;
(* Provide default *)
"a default string")
]} *)
let or_default : 'a Or_error.t -> default:(Error.t -> 'a) -> 'a =
fun t ~default -> match t with Ok v -> v | Error e -> default e
(* Some silly functions to stand in for actual that return different monadic values.
Note how some functions return options, some Or_errors, some Deferreds, etc. *)
let f0 () : int Deferred.t = Deferred.return 1
let f1 () : int Deferred.Or_error.t = Deferred.Or_error.return 1
let f2 () : int option = Some 1
let f3 () : int Or_error.t = Or_error.return 1
let f4 () : int Deferred.t = Deferred.return 1
let f5 a b c d e : int Deferred.Or_error.t = Deferred.Or_error.return (a + b + c + d + e)
(* An example of the sort of "pipeline" style function. *)
let f () : int Deferred.Or_error.t =
let%bind.Deferred (a : int) = f0 () in
let%bind.Deferred.Or_error (b : int) = f1 () in
let%bind.Deferred.Or_error (c : int) =
Deferred.return @@ or_error_of_option @@ f2 ()
in
let%bind.Deferred (d : int) =
Deferred.return
@@ or_default ~default:(fun err ->
(* Stand in for logging *)
prerr_endline ("there was an error: " ^ Error.to_string_hum err);
(* After logging the error, provide a default and keep going. *)
100)
@@ f3 ()
in
let%bind.Deferred (e : int) = f4 () in
f5 a b c d e
Above, f
is an example of the sort of “pipeline” style function. In my project, it might be, make an http request, convert to lambdasoup, use a selector to select a
tags, get another link, make another request, that sort of thing. (In reality, this function probably would be broken up into smaller ones…) The point is, at different “steps” of the pipeline, different monads are used.
Improving it
Now, it seems okay the way it is above. Since, Deferred.Or_error
exists, using ppx_let
along with whatever return
function is appropriate makes things not too bad. Especially if you have a little helper to convert options to or_errors, or utility functions like or_default
to help with logging errors and that sort of thing.
I was wondering though if it could be better. Here are a couple of things I thought of doing.
Lift all helper functions into the Deferred.Or_error monad
For example, if I know the little functions (in this example, f0
through f5
, are only really ever used in the above context, I could change those to accept the right monad and return the right monad, which would avoid some of the let%bind
stuff. (Maybe something similar to what is shown in the railway oriented programming article from F# for Fun and Profit.)
In other word, just lift the small helper functions into the Deferred.Or_error
monad and be done with it.
But I wasn’t sure if that was the right approach either as sometimes the functions are just pretty simple and the monad stuff (error handling, async, whatever) is sort of incidental to what the function itself is handling. Like, do I really want to clutter up an essentially simple function with an Async or Error monad? I’m not sure.
Use a monad transformer library
Could be something like the example in the discuss post here showing how to use a monad transformer library may also be a good option. On the other hand, that may be overkill.
Just stick with monadic-let bindings
Or maybe the monadic-let bindings are the simplest or most convenient way to deal with the problem.
Wrap up
Anyway, I’m not sure the best approach to take (or if there even is a “best” approach), so any advice would be much appreciated!!