[clean-list] Clean in the Real World

Marco Kesseler m.wittebrood@mailbox.kun.nl
Wed, 17 Dec 2003 15:06:06 +0100


Fergus Wrote:
>On 15-Dec-2003, Marco Kesseler <m.wittebrood@mailbox.kun.nl> wrote:
>> Fergus wrote:

[snip]

>> >	foo :: Int
>> >	foo
>> >	    = bar
>> >	    catch DivideByZero = baz

[snip]
>
>Suppose the expression "a + b" occurs in the definition of "bar".
>I can't safely apply equational reasoning to replace this with "b + a",
>because that might change the value returned from foo.
>
>Or, if we consider a slightly different example,
>where foo takes a parameter x,
>
>	foo :: Int -> Int
>	foo x
>	    = bar x
>	    catch DivideByZero = baz x
>
>you may have "foo (a + b)" being different than "foo (b + a)".

Ah, of course: laziness. How stupid of me (and I do use it all the 
time, in many ways...). My apologies.

Still: I REALLY do not like the Monadic solution. Sorry about that 
too, but that is something I cannot help. This is NOT a Monad thing 
in particular: I wouldn't like a "Unique World" solution either.

Now there are a few things worth considering (in order of preference):

* use something else than stack trimming. For _handling_ exceptions 
it is not that important that things go blazingly fast. However, I am 
afraid that things like (a) defining an order over exceptions and 
delivering the largest or (b) delivering the complete set of 
exceptions are no serious options either. At this moment, I cannot 
oversee what it would do to ordinary execution, but I am sure that 
you (Fergus) have looked into this.

* Last resort solution: basically, allow only a single exception 
type, so that handlers cannot detect what hit them. Useful for some 
things, but not all.

* Hurt referential transparency. Hmmmm. The only thing that is worse is:

* Use another programming language.

Now that I have admitted defeat, there is more (yes there is)... 
Please keep in mind: I am not defending lack of referential 
transparency below.

>> (b) have compiler optimisations both in bar and baz, as it does not 
>> matter which exception gets thrown in bar, as long as it is the same 
>> one each time within a single program.
>> 
>> The one thing that is not possible, in terms of optimisation, is to 
>> simply replace occurences of foo by bar. You cannot, because foo does 
>> not just equal bar, and there is no notation to attach the handler to 
>> a loose bar. And you should not because then the compiler would have 
>> to ensure that each of these occurences of bar employs the same 
>> evaluation order.
>
>That's not the only thing which is not possible, in general.
>You also can't easily specialize occurrences of foo.
>That's not important for that original example,
>but consider another slight variation:
>
>	foo :: Int -> Int -> Int
>	foo f
>	    = bar f
>	    catch DivideByZero = baz f
>
>If "bar" calls "f" often, and there is a call to "foo (\ x y -> x + y)",
>then it may be very important for performance to specialize both
>"foo" and "bar", so that the calls to f can be inlined to a single
>cheap add instruction rather than the expensive sequence of instructions
>needed for a call to an unknown higher-order function term.

Yes, but actually I would not start being concerned about the 
efficiency of functions that contain exception handlers. I think the 
"efficiency" remarks take away the attention from the more 
fundamental issues. The more because it remains to be seen how often 
the example above appears in practice.

I do agree however that the exception mechanism should not affect 
ordinary evaluation (badly). With this, I mean evaluation that does 
not in any way goes through a function with a handler.

>Well, I suppose the compiler could do the specialization, provided that it
>was done _after_ any optimizations that change the order of evaluation.
>But normally the compiler should do such specializations first, and then
>apply all the other optimizations, such as inlining, constant propagation,
>etc., so that the specialized code can be simplified.
>
>> I am not willing to violate referential transparency (just yet). If I 
>> were, this discussion would not have become this long. Speaking of 
>> referential transparency: what is so referentially transparent about 
>> the Monadic approach:
>> 
>> getException x >>= (\v1 ->
>> getException x >>= (\v2 ->
>> return (v1 == v2)
>>
>> Why question this? For the simple fact that "getException x" does not 
>> deliver the same value each time.
>
>"getException x" always evaluates to the same value, an IO action.

Yes, I know. But that does not help reasoning about the value you get 
back. That's what I mean if I say that the proof of the program is in 
the programming. The syntax above is equivalent to:

v1 = getException(x);
v2 = getException(x);
return v1 == v2;

In terms of the official semantics, this may be referential 
transparant. In terms of what the programmer sees and experiences, it 
is not. THAT is something that I do not like at all.

>When you talk about "delivering" a value, you mean applying this action
>to some particular world state to get a concrete value rather than
>an IO action.  If you apply the action to two different states then
>naturally you may get different values delivered.  That is the case in
>your example above.
>
>> >If we have
>> >
>> >	foo :: Int
>> >	foo
>> >	    = bar
>> >	    catch _ = baz
>> >
>> >	bar = (1/0) + loop
>> >
>> >and the compiler evaluates the addition left-to-right, then "bar"
>> >will throw a DivideByZero exception, and so "foo" will return "baz",
>> >but if the compiler evaluates the addition right-to-left, then neither
>> >"foo" nor "bar" will terminate.
>> 
>> Well, luckily our programs only have to be referentially transparent 
>> if they terminate.
>> 
>> In practice, this is a non-issue.
>
>If the compiler transforms a terminating program into a non-terminating
>one, that is usually considered to be a rather serious issue :)

Do you call this a terminating program? That's indeed a rather 
serious issue :-)

In the lamda calculus, there are no exceptions. Just diverging 
computations. If you have written such a program, it already _is_ 
officially non-terminating. It just happens that the system is nice 
enough to sometimes catch one of these and tell you.

And it does not really matter whether the compiler makes it 
non-terminating, or some non-determism monad at runtime. It is not 
like the Monadic approach will spare you from non-termination. This 
program is a recipe for disaster. It's a deceased program. If you 
were a hired programmer, you'd be lucky if your boss does not find 
out. That's why it is a non-issue.

Now I come to think of it... I could hide behind the argument that if 
foo(a + b) <> foo(b + a) that this was caused by a diverging 
computation that the system happened to catch. And, as lamda calculus 
does not guarantee _anything_ about diverging expressions, the 
exception handler can do anything it likes (call abort, for example).

But that is kind of, how do you say,... a red herring?



>-- 
>Fergus Henderson <fjh@cs.mu.oz.au>  |  "I have always known that the pursuit
>The University of Melbourne         |  of excellence is a lethal habit"
>WWW: <http://www.cs.mu.oz.au/~fjh>  |     -- the last words of T. S. Garp.
>
>
>_______________________________________________
>clean-list mailing list
>clean-list@cs.kun.nl
>http://www.cs.kun.nl/mailman/listinfo/clean-list