A popular meme in the world of PL’s is that “Haskell has monads”, with the implication that this is a distinctive feature of the language, separate from all others. While it is true that Haskell has popularized the use of monads as a program structuring device, the idea of a monad is not so much an issue of language design (apart from the ad hoc syntactic support provided by Haskell), but rather one of library design. After all, a monad is just one of a zillion signatures (type classes) with which to structure programs, and there is no particular reason why this one cannot be used in any language that supports even a modicum of modularity.
Examined from the point of view of ML, monads are but a particular of use of modules. The signature of monads is given by the definition
signature MONAD = sig type 'a monad val ret : 'a -> 'a monad val bnd : 'a monad -> ('a -> 'b monad) -> 'b monad end
There are many, many, many structures that satisfy this signature; I needn’t (and, in any case, can’t) rehearse them all here. One particularly simple example should suffice to give the general idea:
structure Option : MONAD = struct type 'a monad = 'a option fun ret x = SOME x fun bnd (SOME x) k = k x | bnd NONE k = NONE end
This is of course the option monad, which is sometimes used to model the data flow aspects of exceptions, perhaps with some elaboration of the NONE case to associate an exceptional value with a non-result. (The control flow aspects are not properly modeled this way, however. For that one needs, in addition, access to some sort of jump mechanism.)
Examples like this one proliferate. A monad is represented by a structure. Any structure that provides the facilities specified by the MONAD signature gives rise to the characteristic sequentialization mechanisms codified by it. Monad transformers are functors that transform one monad into another, with no fuss or bother, and no ad hoc mechanisms required. Standard modular programming techniques suffice to represent monads; moreover, the techniques involved are fully general, and are equally applicable to other signatures of interest (arrows, or quivers, or bows, or what have you). Moreover, it is shown in my paper with Chakravarty, Dreyer, and Keller how to integrate modules into the type inference mechanism of ML so that one can get automatic functor instantiation in those limited cases where it is self-evident what is intended. This has been implemented by Karl Crary in a prototype compiler for an extension of Standard ML, and it would be good to see this supported in more broadly available compilers for the language.
The bulk of the mania about monads is therefore accounted for by modules. I have no doubt, however, that you are wondering about the IO monad in Haskell. Isn’t that a fundamental feature of the language that cannot be replicated in ML? Hardly! It’s entirely a matter of designing the signatures of the standard basis library modules, and nothing more. The default basis library does not attempt to segregate effects into a monad, but it is perfectly straightforward to do this yourself, by providing your own layer over the standard basis, or to reorganize the standard basis to enforce the separation. For example, the signature of reference cells might look like this:
signature REF = sig type 'a ref val ref : 'a -> 'a ref IO.monad val ! : 'a ref -> 'a IO.monad val := : 'a ref -> 'a -> unit IO.monad end
Here we are presuming that we have a fixed declaration
structure IO : MONAD = ...
that packages up the basic IO primitives that are already implemented in the run-time system of ML, more or less like in Haskell. The other signatures, such as those for mutable arrays or for performing input and output, would be modified in a similar manner to push effects into the IO monad. Et voila, you have monadic effects.
There’s really nothing to it. In fact, the whole exercise was carried out by a Carnegie Mellon student, Phillippe Ajoux, a couple of years ago. He also wrote a number of programs in this style just to see how it all goes: swimmingly. He also devised syntactic extensions to the Moscow ML compiler that provide a nicer notation for programming with monads, much as in Haskell, but better aligned with ML’s conventions. (Ideally it should be possible to provide syntactic support for any signature, not just monads, but I’m not aware of a worked-out design for the general case, involving as it would an intermixing of parsing and elaboration.)
My point is that the ML module system can be deployed by you to impose the sorts of effect segregation imposed on you by default in Haskell. There is nothing special about Haskell that makes this possible, and nothing special about ML that inhibits it. It’s all a mode of use of modules.
So why don’t we do this by default? Because it’s not such a great idea. Yes, I know it sounds wonderful at first, but then you realize that it’s pretty horrible. Once you’re in the IO monad, you’re stuck there forever, and are reduced to Algol-style imperative programming. You cannot easily convert between functional and monadic style without a radical restructuring of code. And you are deprived of the useful concept of a benign effect.
The moral of the story is that of course ML “has monads”, just like Haskell. Whether you want to use them is up to you; they are just as useful, and just as annoying, in ML as they are in Haskell. But they are not forced on you by the language designers!
Update: This post should’ve been called “ML Has Monads, Why Not?”, or “Of Course ML Has Comonads!”, but then no one was wondering about that.
Update: I now think that the last sentence is excessive. My main point is simply that it’s very simple to go one way or the other with effects, if you have modules to structure things; it’s all a matter of library design. A variant of ML that enforced the separation of effects is very easily constructed; the question is whether it is useful or not. I’ve suggested that the monadic separation is beguiling, but not clearly a great idea. Alternatively, one can say that we’re not that far away from eliminating laziness from Haskell, at least in this respect: just re-do the standard basis library in ML, and you’re a good ways there. Plus you have modules, and we understand how to integrate type classes with modules, so the gap is rather small.
Update: Removed inaccurate remarks about unsafePerformIO.