IO Monad for OCaml

Has there been any efforts in creating an IO Monad in OCaml? I was surprised when I looked for one and couldn’t find one. OCaml isn’t haskell but effect based programming can still be useful in the OCaml land, isn’t it?

2 Likes

Is IO one of the more useful monads in Haskell? I’ve always thought it was more trouble than it was worth.

There are a few implementations of concurrency monads in OCaml, but in general Ocaml code is pretty monad-free.

I can think of only one kind of monad that I’ve ever used in Ocaml code: the “result” monad, which captures a result, or an exception. In a type-checker, I wanted to cleanly switch between (a) stopping at the first exception, and (b) typechecking as much as possible, gathering all exceptions until the end. But that’s it.

I won’t bother citing the reasons that I think Ocaml code typically doesn’t use monads, b/c wasted screen space – it’s been discussed to death.

1 Like

On the topic of controlling effects including IO, you might be interested in https://www.janestreet.com/tech-talks/effective-programming/

4 Likes

[I no longer work in PL, having left it for transaction-processing, distributed systems, and “systems” 20+ years ago, but]

I still remember Xavier Leroy’s PhD thesis (specifically, the introduction) in which (as I remember it, and having re-read it just now, I don’t think this is inaccurate) he argues that complex effect-based type systems expose too much information from a module to its consuming client code. I thought this argument was correct in 1994, and I don’t really see how time has changed that assessment.

Maybe these new effect-typing systems are able to negate that argument: I guess it remains to be seen. But it’s always been one of my reservations about the use of monads (e.g. in Scala) – people (even people I respect) tell me that it allows them to “more precisely model the types of code” but it seems to me that such “precision” is … (uh) precisely (ha!) what Xavier was arguing was not such a great thing, in that introduction.

Again, I no longer work in PL, so maybe things have changed, and it’s all better now.

OCaml isn’t haskell but effect based programming can still be useful in the OCaml land, isn’t it?

I find monadic constructs very useful in OCaml, and they are frequently (but judiciously) used in plenty of code I consume. The common standard library replacements Base and Core supply the usual monad operators and related helpers for the usual suspects (Result, Option, List, probably others I’m unaware of). As @jfeser notes, both of the principle concurrency libraries Async and Lwt manage sequencing of blocking opertions via monad operators. See also http://binaryanalysisplatform.github.io/bap/api/v1.6.0/Monads.Std.html and the results of opam search monad.

As you may know, Ocaml 4.08 adds support for binding operators, and, afaik, the principle motive was to provide nice syntax sugar for applicative and monadic code (as demonstrated by the examples given in the docs).

I am, however, not aware of a monadic library for regular IO. I have heard it said that “OCaml is always in the IO monad”, so maybe a monadic IO library would mean opening a module that shadowed and disallowed all impure functions and modules from the standard library, forcing code to use a provided monadic interface? That could be interesting, and I might experiment with such a thing if it were available, tho I am skeptical whether it would be worth the effort and doubt there would be very much interest from the existing OCaml community. In my experience, OCamlers aren’t shy about using impure or imperative code when it is expedient and safe. :slight_smile:

6 Likes

Chet, don’t put words in my mouth :slight_smile: Back in 1992 I didn’t really know about monads… Today, I think monads can be extremely useful in OCaml, for things like lightweight concurrency (LWT), CPS programming, backtracking, etc.

Nonetheless, it is true that if all you want is state, exceptions,or I/O you could just as well use the built-in state, exception,or I/O mechanisms; going through a monad will not buy you much by itself.

So, to try to answer the original question: yes, you can define an I/O monad pretty much like in Haskell (e.g. as a state monad where the state is just (), the unit value), but the benefits are unclear compared to just using I/O functions without monadic encapsulation.

8 Likes

Even if you did that, you wouldn’t be able to prevent effects in normative OCaml code due to the fact that it’s part of the syntax itself.

1 Like

In fact, I once suggested @ivg to extract the BAP Monads library into the separate, opam-published library.

The monads library is, and always was, a separate, opam-published library :slight_smile:

1 Like

Now on topic. The IO Monad is an ugly and furry byproduct of fighting between the beautiful pass-by-name purely mathematical semantics of Haskell and the ugly real world. The IO Monad is still cute, because it is furry, other solutions were ugly naked. The fur hides the ugliness.

Let’s shave it. The semantics of Haskell is very weak1, it doesn’t guarantee the order of execution, the execution itself (i.e., that a value is evaluated at all), and reserves the right to rearrange any computations in any order which preserves explicit data dependencies. This gives a lot of power to the language implementer, which, unfortunately, had to be stolen from the language user. And the mere existence of the IO Monad showcases this.

The language designers still need to give their uses an ability to tell - “this must be called after this, in that order”. Without that it would be impossible to write computer programs that manipulate and interact with the real world, since in the real world the order of events matters. That is basically the raison d’être of the IO Monad. The IO Monad is the only way to impose an ordering on computations.

On the other hand, OCaml has a much stronger semantics, it precisely defines the order of evaluation and let the computations have any side-effects and guarantees that it won’t reorder them in a way which will be observable. This steals a lot of power from the hands of the language implementer, making it much harder to implement the compiler, but gives this power to the users. This a fair trade off, since usually most of the human effort is spent on using the language, rather than developing it. I.e., the compiler is written once, but used many times. So it is probably fine to invest a lot in the compiler.

Such strict semantics, comes with a price of its own. Not only the developers of the language is paying it, but unfortunately the users have to pay their share. Things like language restriction, lack of referential transparency, monocore implementation, and missed optimization opportunities by the compiler, is the price that we, as the language users, have to pay for the impurity. So, if we’re already paying this price, what would be the reason for not using what we payed for? It would be like buying a plane ticket and not taking the flight, preferring a sailboat instead, as sails are pure and do not pollute the environment. But we already paid the fare, and the plane is still flying, just without us.

But enough metaphors. Let’s assume we will write an IO Monad in OCaml. In fact, I believe, that a few people did this, given how trivial it is to implement it in OCaml with its strict semantics. The main question is whether it will receive any traction? Probably not, especially, since our IO Monad will be subsumed by the Async monad without being asynchronous.

So let’s implement the IO Monad right now and here. Given that OCaml is strict and that the order of function applications imposes the order of evaluation, the IO Monad is just a thunk, e.g.,

type 'a io = unit -> 'a

A pure value could be lifted into the IO Monad as

let return x = fun () -> x

and the computations are chained using the bind operator

let (>>=) c1 c2 = fun () -> c2 (c1 ())

And this is actually the Fun monad, which is so useless that I even forgot to add to the table of contents in the Monads Library :slight_smile: Indeed, in OCaml, it looks superfluous, because of its meta-circular definition, it says that an application is an application, roughly. And lifting an effectful computation into the IO monad distills just to adding an extra () argument, e.g.,

let print_endline x () = print_endline x

But, let’s not underestimate the power of abstraction, as indeed, we can still benefit from the fur that monads give us and hide the fact that IO is just a thunk. This will make our IO operations explicit and employ the OCaml typechecker to track side-effects for us. Sounds good, let’s use the Monads library for that. E.g.,

open Monads.Std
module IO = Monad.Fun
open IO.Syntax

let print_endline s = 
  !$print_endline !!s

Now, we have a tedious task of lifting every single operation into our IO Monad. But wait, we have exceptions, which we don’t want to escape from our monad. They are side-effects anyway. So let’s sprinkle the exception monad, fortunately, Fun is also a transformer, so let’s just mix it in,

module IO = Monad.Fun.Make(Monad.Result.Exception)

Now we have a full-fledged IO Monad, which is totally subsumed with the Async monad. So why would anyone prefer it to async, given that async offers more and is also asynchronous?

Ah, probably because we forgot to add variables. Well, that’s easy, we can mix in the state monad, with the state represented as a heterogenous map, e.g., Univ_map.t from the Base library. And our reference type will be the type 'a var = Type_equal.Id.t … but this is the particular point where our nice IO monad is turning into an ugly monster.

So first of all, what we did is wrote a metacircular embedding of OCaml inside of OCaml with the only benefit of having an explicit denotation of side-effects in the computation type, i.e., given proper discipline (i.e., assuming a user won’t use built-in function such as print_endline) we can guarantee that any side-effectful computation will have the 'a io type. But unfortunately, this won’t have any meaning to the compiler itself, which won’t be able to benefit from our pure code and generate better machine code, as the compiler can’t trust in users’ discipline.

However, the most important part is that we were able to implement the IO monad using pure OCaml, while it is impossible to implement the IO Monad in Haskel. Basically, in Haskell, the IO monad is a language primitive. A primitive that is very fat and mixes too many concepts in one cocktail, including the order of evaluation, side-effects, and state. Too much to my taste. OCaml lets us build much nicer abstractions, that included only things that we need. So we don’t really need the IO Monad, instead, we can have monads that do what we want. Besides, OCaml has a rich module system with powerfull abstraction mechanisms, so monad is not the only way to add some fur to your computations to make them fancy. But this is a completely different story.


1) Weak doesn’t mean bad here, I’m using this word with the precise mathematical meaning, Haskell is as a weaker theory than many other languages, as Haskell gives us very few guarantees. This weak theory is leveraged by the language, as much more assumptions could be made - in other words, the language implementer is having much more freedom, than in coventional languages with strict call-by-value semantics.

35 Likes

This reply is great ! I would like to know whether algebraic effects could change the game with respect to that matter. For example, in a world where ocaml users are forbidden (by some compiler option or whatever) to use “real” side-effect and restricts themselves to algebraic effect for their need, I would expect we could get some more involved optimization from the compiler. But it also seems to me (maybe I am incorrect) that this still allows for a lot of flexibility for the user. For example, I think that implementing lazy computation is feasible with algebraic effect (I may be incorrect on that too) but it seems to me that it is as much a built-in feature of Haskell as is the IO monad (again, not sure about that). Would you be so kind as to elaborate ?

For one, monads were not supported by the OCaml syntax (out-of-the-box) until very recently.

Monads are supported just fine before, just the new syntax makes it more pretty is all.

4 Likes