He notes that the standard approach (modules and functors) has some downsides (syntactically heavy; and unlike plain functions where type inference is automatic and works well, modules often require explicit type annotations and sharing constraints; moreover, even if we have first-order modules, in practice first-order modules seem to lack a few features that “normal” modules have, so code where the module implementation depends on runtime information is hard to write).
My question is: if I really want to write code that somehow is parametric over the monad involved, are there alternatives to modules and functors? Is there some clever encoding using plain functions? Or something else (typeclasses?)?
I played around with a “generic” monad ('a,'m)m = ('a -> 'm) -> 'm but didn’t get far.
I also tried the GADT approach via a generic/free/syntactic monad (type 'a m = Return: 'a -> 'a m | Bind: ('a m) -> ('a -> 'b m) -> 'b m), and then writing a map from this to the particular monad.
I’m not sure I understand your question, but in OCaml there are three kinds of records : plain records, objects (which are extensible records with subtyping relation) and modules (which are extensible records with, possibly, type declarations).
Take a look on a simple example :
type pt_record = {x : float; y : float}
module type pt_module = sig val x : float val y : float end;;
type pt_record = { x : float; y : float; }
module type pt_module = sig val x : float val y : float end
let p = { x = 1.5; y = 2.5}
module P : pt_module = struct let x = 1.5 let y = 1.5 end;;
val p : pt_record = {x = 1.5; y = 2.5}
module P : pt_module
p.x = P.x;;
- : bool = true
Here there was no interest to use a module instead of a simple record. Now, take a look at the signature of a monad :
module type Monad = sig
type +'a t
val return : 'a -> 'a t
val bind : 'a t -> ('a -> 'b t) -> 'b t
end
This signature not only define value but also a parametric type, and the module system allows to abstract over parametric type : we can do higher-kinded polymorphism. You can’t do such polymorphism with simple records, the canonical way is to use the verbose language of modules and functors. The best you can do with record type is to define a singular monad type but not a generic one, like in this example :
type option_monad = {
return : 'a. 'a -> 'a option;
bind : 'a 'b. 'a option -> ('a -> 'b option) -> 'b option;
}
let return x = Some x in
let bind m k = match m with None -> None | Some x -> k x in
{return; bind};;
- : option_monad = {return = <fun>; bind = <fun>}
This is not very useful since you can’t write code with a generic monad as parameter. And if you try to write a generic type for monad using record, the compiler will complain with a syntax error :
type 't monad = {
return : 'a. 'a -> 'a 't ;
bind : 'a 'b. 'a 't -> ('a -> 'b 't) -> 'b 't
};;
Error: Syntax error
You don’t want to hide the fact that your functions return something of type 'a Lwt.t or 'a Async.Deferred.t. That’s the one thing that will make your users chain computations together in the monad.
The monad itself is not the most important thing to abstract. What you should be asking is about libraries that provide abstractions over common tasks you need async for. Cohttp is one such library. If your project is simple enough, I would say using module builders is not so bad.
If you haven’t created a parametric library like this before, I strongly recommend you to try. It is a good opportunity to learn many nuances about the module system, and that will make you way more productive in OCaml.
There’s higher. A conceptually satisfying but still syntactically heavy alternative to writing monad generic code.
Effects also seems be a very promising approach that might be available in the not too distant future. The idea here would be that non-blocking applications would raise the same effects and different handlers would correspond to Async’s/Lwt’s schedulers.
The biggest problem I ran into with this is specific use-case that they are really two very different libraries, particularly when it comes to error handling (Lwt includes errors in the monad, and Async prefers monitors instead). So in practise, you have to be very careful about how you use the abstraction. It works ok in Cohttp for parsing, but the majority of the code is in the Lwt- or Async-specific backend. A more modern alternative is probably to remove the IO signature and use Angstrom and Faraday instead.
An alternative is to define your own monad in the “pure” part of your library, and then implement a run function for it in the Lwt or Async application of the pure library.
Regarding the mismatch between the two monads: yes, I agree. So actually my post has a bad title - in my specific use case, I can make the monads match up nicely. I just don’t want to write 2 versions of the code that makes use of these monads.
An alternative is to define your own monad in the “pure” part of your library, and then implement a run function for it in the Lwt or Async application of the pure library.
Thanks. Yes, this seems like the best way to go (pending my read of the “higher” paper).
I once did something similar to what @avsm suggests, and implemented a larg-ish project in CPS. I then added thin translations from CPS to promise/monadic-style code.