[E-Lang] I/O with monads

Jonathan A Rees jar8@mumble.net
Fri, 31 Aug 2001 20:07:15 -0700


[Forwarded with permission.  --MarkM]

I feel my posts have been very muddy.
Below is yet one more attempt to articulate the analogies I see
between nonblocking and monads.

If you find it helpful, forward it as you find appropriate.  I'll
probably put it on my web site.

However, I still don't see how monads might be helpful to E.  I have
looked at I/O monads in Haskell, and it still seems to me that when/catch
discipline plays a completely analogous role in E.  Input, certainly,
should be done by poking the input device using -> and then waiting
for the input using when/catch.  Output should be the same as sending
a message to an output device, and should probably be the same
syntactically, so you should write
  myConsole <- println("foo")
or
  stdout <- println("foo")
instead of just println("foo").  I haven't looked to see if this is
what you're already doing in E, but I wouldn't be surprised if it
were, at least at some level.

- Jonathan

-----
. In Pascal (as defined originally), functions can neither observe nor
  alter state that's external to their invocation.
  (A function may use assignment and so on internally, but it can't read or
  write state that exists outside the function invocation.)
  
. If you wanted a function f to be able to observe or write external
  state, you'd have to write f as a subroutine, and
  separate the call to f from the consumer of f's result by
  putting them in separate commands.  That is, instead of g(f(x)), 
  you'd write
    call f(x,y);    (* y is passed by reference *)
    ... g(y) ...;

. Any result to be communicated would have to be passed out via a
  by-reference parameter -- that is, it would have to be named
  explicitly.

. Pascal partisans consider this good discipline.  If something's
  defined as a function, you know that it's going to be pure.  That
  fact, they argue, makes programs much easier to reason about, and
  are likely to have fewer bugs than programs written without this
  discipline.

-----
. In E, a function cannot block (wait for an event).

. If you wanted a function f to be able to block (assume WLOG that the last
  thing it does is wait for some event), you'd have to
  separate the call to f from the consumer of f's result using a
  when/catch.  That is, instead of ... g(f(x)) ..., you'd write
      when (f <- run(x)) -> y { ... g(y) ... } ...

. The result of calling f is named explicitly in the "when" form for use
  by its consumer.

. E partisans consider this good discipline.  All language constructs
  are non-blocking.  That fact, they argue, makes concurrency
  properties of programs much easier to reason about, and programs are
  likely to have many fewer locking bugs than programs written without
  this discipline.

-----
. In Common Lisp, unlike Scheme, there is no way for a function to capture
  the current continuation as an object, or to discard it.

. If you wanted a function f to be able to capture its continuation,
  you'd have to write f in continuation-passing style, and pass it
  its continuation explicitly as a function.
  Instead of nesting the call to f inside a context that uses it, you
  write the context of use as a separate function.
  That is, instead of ... (g (f x)) ..., you'd write
       (f x #'(lambda (y) ... (g y) ...))

. The result of calling f becomes the argument to the 
  explicit continuation, and is therefore named explicitly.

. Common Lisp partisans consider the lack of continuation reification
  to be a good thing.  They argue that not only does continuation
  reification have a significant cost, their use can make programs
  very difficult to reason about as well as harder to compile well.

-----
. In Haskell, functions can't have side effects, do I/O, or use any
  other "impure" constructions.

. If you wanted to write a function f that did something impure, you'd
  have to separate the call to f from the consumer of f's result
  using a monad M.  That is, instead of ... g(f x) ..., you'd write
       f(x) >>= (\y -> ... (g y) ...)
  The ">>=" is needed because f returns a command but g needs to take
  a value.  ">>=" takes care of combining the two commands f(x) and
  whatever is delivered by g(y)'s context.

  Note that >>= is specific to M, but Haskell figures out what it
  should be based on f's type (or so I infer from
  http://www.dcs.gla.ac.uk/~nww/Monad.html).

  Some syntactic sugar (a la Moggi's "let") makes uses of ">>=" look
  a bit more conventional:
         do y <- (f x) in ... (g y) ...
  [i.e.        "do var <- command1 in command2"
  abbreviates  "command1 >>= (\var -> command2)" ]
  [Haskell actually uses line breaks and indentation instead of "in".]

. The result of calling f is named explicitly as the argument to the
  function that is bind's second argument.

. Haskell partisans consider functional style to be good discipline.
  They argue that expressions should denote
  values, period, and that when this is the case,
  programs are more likely to be easy to reason about and manipulate.
  In particular, many
  useful formal properties hold, such as uniform validity of
  substitution when inlining a function, and erasure of expressions
  whose values aren't used.