IO Monad - SML
Table of Contents
1. Introduction
We typically abstract away effects in a monad, and IO is one of the most basic effects for programmers.
Recall the basic idea for using monads: if we have a function with a
side-effect (like printing a message to the screen), then let us
denote its type with a squiggle arrow f : A ~> B
. Pure functions
without side-effects would be written as pure_fn : A -> B
with the
usual arrow.
Suppose now that we wanted to curry a function with a side-effect
f : A * B ~> C
, what would its type look like? We would guess it
either looks like g : A -> (B ~> C)
or h : A ~> (B ~> C)
.
We would also expect applying only one argument would not produce any
side-effect, only when both arguments are provided would a side-effect
be produced. This would be the first case, g : A -> (B ~> C)
describes
f : A * B ~> C
curried.
Now, we can recall () * A
is isomorphic as a type to A
. This means
that f : A ~> B
is isomorphic to a function with type f2 : A * () ~> B
.
Currying this function gives us g2 : A -> (() ~> B)
. Now we
introduce a monad with type
B impure = () ~> B
which means we have our impure function isomorphic to
the pure function g2 : A2 -> B impure
.
For IO operations, the monadic bind amounts to ordering the IO operations and executing them in the order given. For our purposes in this exploration, it suffices to explore teletype IO (i.e., printing a string to the terminal, reading text from the keyboard, and nothing else).
In an eager language, the IO monad is a weak monad. This is proven in chapter 8 of Andrew Gordon's PhD dissertation. Think about the situation when a bottom value has been encountered, but will not be needed in the next IO operation. Lazy languages can carry on without a problem. Eager languages cannot.
However, lazy IO breaks equational reasoning…which is pretty much why we would want monadic IO (for the equational reasoning!).
The IO monad makes sense for a single-core processor, but how would we extend this to work with multiple threads executed in parallel?
Our implementation will also be incomplete compared to what, say, Haskell offers. There are avenues for further investigation.
Would it be possible to extend the IO monad implemented in Standard ML
to include IORef operations? If so, could we do it without relying on
native ref
types?
2. Andrew Gordon's IO Monad
We can construct the IO Monad in Standard ML, which has been done in Andrew Gordon's PhD thesis. There the construction uses an abstract type:
infix 1 >> >>= abstype 'a Job = JOB of unit -> 'a with (* exec : 'a Job -> 'a *) fun exec (JOB f) = f () (* return : 'a -> 'a Job *) fun return x = JOB (fn _ => x) (* (>>=) : 'a Job * ('a -> 'b Job) -> 'b Job *) fun (JOB f) >>= q = JOB (fn _ => exec (q (f ()))) (* getStr : int -> TextIO.vector Job *) fun getStr n = JOB (fn _ => TextIO.inputN(TextIO.stdIn, n)) (* putStr : string -> unit Job *) fun putStr s = JOB (fn _ => print s) end (* (>>) : 'a Job * 'b Job -> 'b Job *) fun p >> q = p >>= (fn _ => q); (* gettingLine : string -> string Job *) fun gettingLine s = getStr 1 >>= (fn c => if c = "\n" then return(s) else gettingLine (s^c)); (* getLine : string Job *) val getLine = gettingLine ""; val main : unit Job = putStr "First name: " >> getLine >>= (fn first => putStr "Second name: " >> getLine >>= (fn second => putStr ("Hello "^first^" "^second^"\n"))); (* exec main; *)
3. Using Modules
We can encode the IO Monad using SML's Module system. The signature for a Monad would be:
signature MONAD = sig type 'a m; val return : 'a -> 'a m; val bind : 'a m -> ('a -> 'b m) -> 'b m; end;
The IO Monad requires a few more public-facing methods.
signature IO_MONAD = sig include MONAD; val exec : 'a m -> 'a; val getStr : int -> TextIO.vector m; val putStr : string -> unit m; end;
Observe we just switched the Job
constructor to m
. The
implementation details are straightforward.
4. Concluding Remarks
We can also implement an IO monad using Free monads, since free monads provide a way to abstract away impure functions inside a pure monad. This would not be performant in Haskell.
But there is also a way to implement an IO monad using monad transformers. This is what the PureIO proof-of-concept library does in Haskell.
We have so far not discussed blocking IO too much, but that's because blocking IO comes about from threads and multiple processes. Essentially something else is reading or writing to a file (or stream or socket or…) and we have to wait until the resource is free. When will it become important to model this in an IO monad?
5. References
- Andrew Gordon,
"Functional Programming and Input/Output".
Ph.D. Thesis, Cambridge, 1994; Eprint. - Andrew Gordon,
"PFL+: A Kernal Scheme for Functions I/O".
UCAM tech report 160, dated February 1989 PDF - Simon Peyton Jones,
"Tackling the awkward squad: monadic input/output, concurrency, exceptions, and foreign-language calls in Haskell".
in Engineering theories of software construction, IOS Press, 2001; Eprint and pdf.- Discusses the IO monad using a "two tier" semantics, with process semantics for the IO part and some denotational semantics for a lazy lambda calculus.
- PureIO — a pedagogical IO monad implemented in Haskell
- free-io/Free.hs implementation using Free monads