[E-Lang] I/O with monads
Jonathan A Rees
jar8@mumble.net
Wed, 22 Aug 2001 23:13:07 -0400
Date: Wed, 22 Aug 2001 16:08:06 -0700
From: Darius Bacon <darius@accesscom.com>
Jonathan Rees wrote, about monads:
> if fragment1 is a function from values to extensions
> and fragment1 [fragment2 actually] is a function from values to extensions
> then bind(fragment1, fragment2)
> is a function from values to extensions
In the Haskell literature I've seen, fragment1 is a monad (i.e. an
extension) rather than a function from values to monads.
You're right about Haskell's bind being different from mine. However,
to nip confusion in the bud, this isn't how the word "monad" is used
in the literature. "Monad" is something like the side effect feature
*in general*, while an "extension" is something reflecting a state of
the computation; in addition to delivering a value also does
monad-stuff.
Moggi uses the word "computation" for what I call an "extension".
I'm still not sure how monads help with I/O problems. Things like the
ability to send output to a particular display are exactly what
capabilities are for, but this can be handled in the usual way.
If it were me I'd create a value or set of values representing the
ability to do I/O (and other things) on the computer running a vat,
and make them available in some initial interaction environment. To
allow loaded programs, or remote ones, to also use these things would
require explicit transmission of the capability(ies) into the
program's environment.
This is vaguely like Scheme 48's command processor, which gives all
sorts of capabilities (commands) to the user that aren't available to
loaded programs.
Tell me again why I/O monads and/or a functional interaction loop
are/is desirable? What do they buy you, or what risk do they
mitigate? To me, doing I/O monads is equivalent to reifying input
histories and other I/O phenomena. For example, you could get your
hands on a particular position in the stream of all future inputs. If
you passed this off to someone, you could have the same set of inputs
examined by two different programs. Monads are just a notational hack
that might help you feel virtuous, if you're lucky.
-----------------------------------------------------------------------------
Those who don't care about gory monad details can drop out right now.
I couldn't help myself.
A monad in the sense of my last email is any instance of the following
scheme of generic abstract data types:
any particular monad mm has
1. a type constructor mm.M, such that mm.M(A) is a type whenever A is
(something of type mm.M(A) is an "extension" or "computation")
By abuse of notation I'll write M(A) instead of mm.M(A) when mm. is clear.
2. a polymorphic function mm.unit which
for any type A
has type unit: A -> M(A)
3. a polymorphic function mm.bind which
for any types A, B, C
has type (A->M(B)) x (B->M(C)) -> (A->M(C))
or equivalently, if you prefer curried functions,
(A->M(B)) -> [(B->M(C)) -> (A->M(C))]
4. an idiosyncratic library of functions for creating and using extensions.
Haskell's 'bind' probably is
3. a polymorphic function mm.hbind which
for any types B, C
has type M(B) x (B->M(C)) -> M(C)
or equivalently, if you prefer curried functions,
M(B) -> (B->M(C)) -> M(C)
That is, as you say, the first argument to "hbind" is an M(B), not an
A->M(B). This is indeed simpler. My version lets you compose two
functions that don't quite match up (the first delivers an extension
while the second wants a value), while the Haskell version lets you
apply a value-desiring function to an extension.
These definitions are not exactly the same as the definitions used by
mathematicians: instead of "bind" or "hbind", "monad" has
mm.mu: M(M(A)) -> M(A),
while "triple" or "Kleisli triple" has
mm.extend: [B->M(C)] -> [M(B)->M(C)]
But all these forms are completely interconvertible, i.e. "pretty much
the same," and they all have the purpose of sequencing code fragments
that don't quite want to be composed.
For further concreteness: The side effects monad would be defined
something like this:
store_monad.M(A) = store -> (A x store)
store_monad.unit = lambda a. lambda s. (a,s)
store_monad.hbind = lambda (mx, f).
lambda s1.
let (y, s2) = mx(s1) in f(y)(s2)
store_monad.get = lambda location.
lambda s. fetch(s, location)
store_monad.put = lambda (location, value).
lambda s. (null, retrieve(s, location, value))
If you were to look at the result of "get(...)" or "put(...)" or
"bind(f,g)(a)", i.e. at an extension, you'd have something that,
having already received a value and gone as far as possible without
looking at or modifying the store, is ready to take a store and read
and write it as much as it likes.
For a simple exception system, you'd have M(A) = A + Exception (either
or a value or an Exception). For continuations, you'd have M(A) =
(A->Answer)->Answer. For nondeterminism, you'd have M(A) = set of
A's. Don't have I/O at the tip of my keyboard; see Moggi's or
Wadler's papers, or Haskell documentation.