I don’t know what crack I was smoking the other day, but my implementation
of general monads not only does not have the property I claimed
for it, viz. that it provides the same level of type-checking as
Haskell ahead of execution-time, but it cannot
provide such a level of ahead-of-runtime checking, whether the hosting
language is eager or lazy.
To see the first, try the following example:
> (run-io (mlet* ((_ (mdisplay "HERE\n"))
(_ sget))
(return 232)))
HERE
wrong monad-class ((mclass #5(struct:<monad-class> io [...]))
(m #3(struct:<monad> #5(struct:<monad-class> state [...]) [...])))
Note how an action is performed before the type error is
detected. In general, it is not possible to detect the type-error
ahead of time without abstract interpretation of the level of
sophistication that Haskell’s type-checker provides, as the following
example demonstrates:
> (define (goes-wrong)
(run-io (mlet* ((val mread))
(if val
(mlet* ((_ (mdisplay "Was true\n"))) (return 'apples))
'bananas)))) ;; Note: missing "(return ...)"!
> (goes-wrong)
#t
Was true
apples
> (goes-wrong)
#f
<monad>-ref: expects type <struct:<monad>> as 1st argument, given: bananas; other arguments were: 0
[...]
The problem is that the system can decide how to continue based on
I/O performed at runtime, and so the composition performed by bind
cannot be checked until after the left-hand-side has been fully
evaluated. [Aside: the <monad>-ref
error was
a symptom of the unexpected problem described below — now that
the code has been fixed, it instead complains “not a monad
bananas
”, as it should.]
This affects all my monad implementations, not just the IO monad:
> (run-st (mlet* ((a sget)
(b (mdisplay "OH NO\n"))
(_ (sput 4)))
(return (+ a 1)))
2)
OH NO
(3 . 4)
Actually, and this is unexpected ("OH NO"
indeed!),
here it’s even worse than just not catching the type-error before any
actions have executed: it isn’t catching the error at
all. Here’s what that mlet*
expands into:
(>>= sget
(lambda (a)
(>>= (mdisplay "OH NO\n")
(lambda (b)
(>>= (sput 4)
(lambda (_)
(return (+ a 1))))))))
My implementation of >>=
isn’t “reaching into
the lambda” in the case of the IO monad. [Interlude; sounds
of programming from behind the curtain.] The problem was
my use of the (bad) “delayed monad” idea in implementing the IO monad
— I’ve just replaced that idea with a more explicit
representation of a delayed action, and the code now behaves as I
expected. How embarrassing! Here’s the trace now that I’ve fixed that
problem:
> (mlet* ((a sget)
(b (mdisplay "OH NO\n"))
(_ (sput 4)))
(return (+ a 1)))
#3(struct:<monad> #5(struct:<monad-class> state [...]) [...])
This shows that before we run the monad, it appears to be a
well-behaved state monad instance. Running it now causes the error to
be detected:
> (run-st (mlet* ((a sget)
(b (mdisplay "OH NO\n"))
(_ (sput 4)))
(return (+ a 1)))
2)
wrong monad-class ((mclass #5(struct:<monad-class> state [...]))
(m #3(struct:<monad> #5(struct:<monad-class> io [...]) [...])))
This demonstrates what I was hoping to show above, before I was
derailed by the delayed-monad issue: that the impossibility of using
this technique to type-check monad assemblies ahead of any actions
being performed applies to all monad kinds, not just the IO monad. Of
course, it’s less of a problem for non-side-effecting monads, such as
the state monad, since performing an action (in the case immediately
above, reading the hidden state variable) in such a monad has no
effect on the outside world by the time the type-error is
detected.
In conclusion, I was mistaken about Scheme’s ability to achieve the
same level of ahead-of-execution type-checking as Haskell manages
— I don’t believe it to be possible without essentially
Haskell-equivalent abstract-interpretation machinery bolted on to the
language. Nonetheless, the library I’ve developed does catch type
errors eventually — so long as the faulty code is eventually
run! This is no worse than regular dynamically-typed language code,
and is still a useful system, so I’m not completely disappointed. I
will still be able to use the idea to structure side-effects in
ThiNG.