[comp.lang.functional] Purity and Laziness

jrk@sys.uea.ac.uk (Richard Kennaway) (05/25/90)

In article <1990May25.024023.10616@comp.vuw.ac.nz> brian@comp.vuw.ac.nz (Brian Boutel) writes:
>In article <4170@castle.ed.ac.uk>, nick@lfcs.ed.ac.uk (Nick Rothwell) writes:
>> Correct me if I'm wrong (more than likely...) but in a lazy language
>> I can write things like
>> 
>> 	fun f x = cons(1, f x)
>> 
>> to build an infinite list, but not
>> 
>> 	fun f x = reverse_cons(f x, 1)
>> 
>> because of the reduction order.
>> 
>> So it strikes me that lazy evaluation has this implicit evaluation
>> order (with different termination characteristics) which is left
>> to right. Eager evaluation is purer because it doesn't; if you have
>> an infinite computation it hangs regardless.

It's not lazy evaluation which has this evaluation order, but the
so-called lazy languages.  I regard this as a defect, precisely because
it fails to be lazy.  An eager language, in contrast, is uniformly bad
in this respect.  I find the absence of laziness in a functional
language as irksome as the absence of recursion would be in imperative
languages.

>Lazy evaluation *will* allow your reverse_cons example. All the
>arguments of functions and constructors are passed unevaluated.
...
> --brian


> || It's not quite as simple as that.

(This is jrk speaking, from here on the ">" doesnt indicate quoting.
The reason will eventually become clear.)

Although the reverse_cons example doesnt cause problems, other examples do.
Consider the following definitions:

>    list  ::=  Nil  |  Cons num list

>    f Nil Nil           =   1 
>    f Nil (Cons x y)    =   2
>    f (Cons x y) z      =   3

>    g Nil Nil           =   1
>    g (Cons x y) Nil    =   2
>    g z (Cons x y)      =   3

>    loop                =   loop

This is Miranda, but the point can be made in any of the supposedly lazy
functional languages I know.  

Notice that f and g are nearly the same function, but they take their
arguments in the opposite order.  Now consider the two expressions

>   test1   =   f (Cons 1 Nil) loop

and

>   test2   =   g loop (Cons 1 Nil)

test1 evaluates to 3.  Therefore so should test2, right?  But it gives a
non-terminating computation instead.

Functional languages are supposed to have the advantage that a functional
program can be read as a piece of mathematics, to which ordinary
mathematical reasoning can be applied to prove properties of a program.  No
separate proof-theoretic apparatus should be required.  For languages such
as Miranda, ML, Haskell, etc. this means that one should be able to read a
program such as the above as a set of equational axioms.  "Evaluating" a
term is done by using those axioms to prove the term equal to some term
which has a printable representation.

With the above axioms, it is easy to prove test1 and test2 both equal to 3
(and perhaps less obviously, it is impossible to prove either equal to any
other printable value).  Miranda does this with test1, but fails with
test2.  Therefore Miranda is not fully lazy.

Every other functional language I have seen would do at least as badly
(some might fail to compute test1 as well).

The reason for this behaviour is that Miranda adopts a specific evaluation
strategy for any program, which may be summed up as "left to right, top to
bottom".  The evaluation of test 2 first applies the only rule for test2,
replacing it by g loop (Cons 1 Nil).  It then tries the first rule for g. 
If it tried to match the second argument of the rule (which is Nil) with
the second argument of the term (which is (Cons 1 Nil)), the match would
fail and the pattern-matcher could go on to try the second rule, which
would also fail, and then the third, which would succeed and give the final
result 3.  However, pattern-matching in Miranda always goes from left to
right.  In trying to match the first rule for g against (g loop (Cons 1
Nil)), it tries to match Nil against loop.  Seeing that there is a rule for
loop, it applies it, getting the same term again, and goes into an infinite
loop.

I discuss all this at even greater length in a paper presented at the ESOP
conference last week (Springer LNCS 432), but the basic algorithm for
dealing properly with systems like the above has been known for eleven
years (see Huet & Levy "Call by need computations in non-ambiguous linear
term rewriting systems", INRIA report 359, 1979).

BTW, if you have a Miranda implementation, you can give it the whole of
this message, starting at the line which begins "> ||", and see the example
behave as described.

--
Richard Kennaway          SYS, University of East Anglia, Norwich, U.K.
Internet:  jrk@sys.uea.ac.uk            uucp:  ...mcvax!ukc!uea-sys!jrk

geoffb@butcombe.inmos.co.uk (Geoff Barrett) (05/25/90)

In article <4170@castle.ed.ac.uk> nick@lfcs.ed.ac.uk (Nick Rothwell) writes:
>Correct me if I'm wrong (more than likely...) but in a lazy language
>I can write things like
>
>	fun f x = cons(1, f x)
>
>to build an infinite list, but not
>
>	fun f x = reverse_cons(f x, 1)
>
>because of the reduction order.

If you apply a printing function to the second you might get a "(" on
the screen and then the program will hang, but that is to do with the
strictness of the printing function.  It's the printing function which
has defined an evaluation order, in effect.  However, if I were to
pattern match

        fun last (reverse_cons(x,a)) = a

a call of "last (f x)" should return "1" (BTW, what's that "x" for?).

I believe there is an Oxford DPhil thesis by a man called David Turner
which pointed out that strict languages are not referentially
transparent because of the fact that conditionals are not strict, so
that the equality symbol in the definition

        fun iiff (b,x,y) = if b then x else y

is not equality.  Lazy evaluation languages are referentially
transparent.  I think this is what the original poster meant by
"purer".

>		Nick.
>--
>Nick Rothwell,	Laboratory for Foundations of Computer Science, Edinburgh.
>		nick@lfcs.ed.ac.uk    <Atlantic Ocean>!mcsun!ukc!lfcs!nick
>~~ ~~ ~~ ~~ ~~ ~~ ~~ ~~ ~~ ~~ ~~ ~~ ~~ ~~ ~~ ~~ ~~ ~~ ~~ ~~ ~~ ~~ ~~ ~~ ~~
>		   Ich weiss jetzt was kein Engel weiss

						Geoff Barrett


vvv|       |vvv-------------------------------------
    \ o o /
     \   /           Wot! No Queen's Award logo?

fs@rex.cs.tulane.edu (Frank Silbermann) (05/25/90)

In article <1535@sys.uea.ac.uk> jrk@sys.uea.ac.uk (Richard Kennaway) writes:
>
>	Functional languages are supposed to be readable as mathematics.
>	For languages such as Miranda, ML, Haskell, etc. this means
>	that one should be able to read a program such as that below
>	as a set of equational axioms.
>
>    list  ::=  Nil  |  Cons num list
>
>    f Nil Nil         = 1 	 	g Nil Nil         = 1
>    f Nil (Cons x y)  = 2 		g (Cons x y) Nil  = 2
>    f (Cons x y) z    = 3		g z (Cons x y)    = 3
>
>    loop              = loop

You are suggesting that the Miranda's semantics should reflect
the equational notation.  Actually, the logic is not equational;
Miranda permits contradictory program clauses, which are resolved
by the evaluation order.

The clausal notation, which incorporates pattern-matching,
is only a syntactic sugar.  I find pattern-matching
a convenient shortcut when hacking, but when thinking about
denotational semantics, I first compile such syntactic sugars
out of my examples.

>	Notice that f and g are nearly the same function,
>	but they take their arguments in the opposite order.
>	(The results differ in some cases, therefore Miranda's
>	evaluation strategy is not quite correct.)

Only if you take the syntactic sugars literally.

>	... the basic algorithm for dealing _properly_
>	with systems like the above ...(can be found in)...  Huet & Levy
>	"Call by need computations in non-ambiguous linear
>	term rewriting systems", INRIA report 359, 1979.

You are advocating a different kind of language.
If you want to interpret a Miranda program as a
non-ambiguous linear term rewriting system,
you will need a way to prevent contradictions in the equations
(which Miranda permits).  Also, I doubt that the paper you cite
takes higher-order functions into consideration.

	Frank Silbermann	fs@rex.cs.tulane.edu
	Tulane University, New Orleans, Louisianna, U.S.A.

brian@comp.vuw.ac.nz (Brian Boutel) (05/28/90)

In article <1535@sys.uea.ac.uk>, jrk@sys.uea.ac.uk (Richard Kennaway) writes:
> In article <1990May25.024023.10616@comp.vuw.ac.nz>
brian@comp.vuw.ac.nz (Brian Boutel) writes:
> >In article <4170@castle.ed.ac.uk>, nick@lfcs.ed.ac.uk (Nick Rothwell)
writes:
[...]

> 
> It's not lazy evaluation which has this evaluation order, but the
> so-called lazy languages.  I regard this as a defect, precisely because
> it fails to be lazy.  An eager language, in contrast, is uniformly bad
> in this respect.  I find the absence of laziness in a functional
> language as irksome as the absence of recursion would be in imperative
> languages.
> 
> >Lazy evaluation *will* allow your reverse_cons example. All the
> >arguments of functions and constructors are passed unevaluated.
> ...
> > --brian
> 
> 
> > || It's not quite as simple as that.
> 
> (This is jrk speaking, from here on the ">" doesnt indicate quoting.
> The reason will eventually become clear.)
> 
> Although the reverse_cons example doesnt cause problems, other examples do.
> Consider the following definitions:
> 
> >    list  ::=  Nil  |  Cons num list
> 
> >    f Nil Nil           =   1 
> >    f Nil (Cons x y)    =   2
> >    f (Cons x y) z      =   3
> 
> >    g Nil Nil           =   1
> >    g (Cons x y) Nil    =   2
> >    g z (Cons x y)      =   3
> 
> >    loop                =   loop
> 
> [....]
> Notice that f and g are nearly the same function, but they take their
> arguments in the opposite order.  Now consider the two expressions
> 
> >   test1   =   f (Cons 1 Nil) loop
> 
> and
> 
> >   test2   =   g loop (Cons 1 Nil)
> 
> test1 evaluates to 3.  Therefore so should test2, right?  But it gives a
> non-terminating computation instead.
> [....]
> 
> The reason for this behaviour is that Miranda adopts a specific evaluation
> strategy for any program, which may be summed up as "left to right, top to
> bottom".
> 


 I think jrk has done a large issue switch here, but, taking up his point:

Every language of this class has to specify the semantics of
pattern/equation  matching, and do do in a way that will permit
efficient implementation. Otherwise, we will all continue to play with
functional languages and do our real work in C.

It would be highly desirable for Miranda and Haskell to be sufficiently
lazy to guarantee that they would never loop/die/bottom while matching
equations if there is just one equation which matches the actual
arguments. Unfortunately,  examples like

f x     false true  = 1
f true  x     false = 2
f false true  x     = 3

show that there is no sequential algorithm which will always work. For
example, 

f bot false true

 matches the first equation and no other, but if, in matching, the first
argument is matched first, this will bottom. Similarly for f true bot
false if the second argument is matched first, etc. Whatever you try to
match first, there is an argument value that will cause non-termination.

The only ways to avoid this are
a) to outlaw programs like the example, which seem to be otherwise
reasonable, or
b) to go to parallel evaluation, which is expensive to simulate on a
sequential machine.

So, if the goal is to define a useful language for real computation, and
to have lazy evaluation, it seems that some compromise is necessary.
--brian

Internet: brian@comp.vuw.ac.nz
Postal: Brian Boutel, Computer Science Dept, Victoria University of Wellington,
        PO Box 600, Wellington, New Zealand
Phone: +64 4 721000
Fax:   +64 4 712070

smart@ditmela.oz (Robert Smart) (05/28/90)

In article <1990May28.020325.18011@comp.vuw.ac.nz> brian@comp.vuw.ac.nz (Brian Boutel) writes:
> b) to go to parallel evaluation, which is expensive to simulate on a
> sequential machine.
>
I'm a complete amateur at this, but I can't believe that parallel evaluation
would be as bad as all that in practise. It's not like an operating system
where the processes have unknown behaviour and you have to be very fair about
sharing time. All you need is for both processes to call a scheduler every
now and then. You might start with calling the scheduler every 16th time
through a particular calculation point, then move that up to every 32nd, etc.
In that way the scheduling cost would always be low in large calculations.

Also I really doubt whether there are a lot of cases where a procedure has
multiple parameters which might not terminate. Most commonly there will only
be one and the compiler should be able to figure out a reasonable heuristic
for deciding which patterns to try to match for a given function call.
To be popular a language needs to behave in a surprise free fashion. For
lazy functional languages that has to mean parallel parameter evaluation
when truly necessary.

Bob Smart

geoffb@butcombe.inmos.co.uk (Geoff Barrett) (05/29/90)

In article <BJORNL.90Jul26211631@tarpon.tds.kth.se> bjornl@tds.kth.se (Bj|rn Lisper) writes:
]In article <7044@ganymede.inmos.co.uk> geoffb@butcombe.inmos.co.uk (Geoff
]Barrett) writes:
]%I believe there is an Oxford DPhil thesis by a man called David Turner
]%which pointed out that strict languages are not referentially
]%transparent because of the fact that conditionals are not strict, so
]%that the equality symbol in the definition
]
]%	fun iiff (b,x,y) = if b then x else y
]
]%is not equality.  Lazy evaluation languages are referentially
]%transparent.
]
]Hmmm. How would a lazy language treat a call
]
]if(b,x,x)
]
]where the evaluation of b is non-terminating but the evaluation of x
]terminates? If "naive" left-to-right evaluation order is assumed, the
]computation will not terminate. On the other hand, it is reasonable to
]define the semantics of the if-function so that such a call returns the
]value of x. (After all, the value of x will be the answer regardless of what
]b evaluates to). What about "referential transparency" here?

I think I need a good deal of convincing that it would be "reasonable"
to define the semantics of "if(b,x,x)" to be "x".  But, even so, so long
as "iiff (b,x,x)" and "if b then x else x" evaluate to the same thing
then the language is still referentially transparent.  All I want to be
able to do is to substitute equals.

]This problem is akin to left-to-right evaluation sensitivity of goals in
]Prolog; the order of goals that are ANDed together in a Horn clause can
]affect termination even though AND, considered as a "mathematical" binary
]operation, is commutative.

One would hope that it is commutative on proper values and obeys the
same rules as the "mathematical" operator there.  The functions

   f x = 2*x; g x = x/2

satisfy the rule "f.g = id = g.f" if their domains are restricted to the
non-zero real numbers, but not on the whole of the real numbers.
Sometimes different functions do satisfy different laws.  I think this
is what the French might call a "hering rouge".

]Bjorn Lisper

						Geoff Barrett
Newsgroups: comp.lang.functional
Subject: Re: Purity and Laziness
Summary: 
Expires: 
References: <14531@dime.cs.umass.edu> <2535@skye.ed.ac.uk> <8691@cs.utexas.edu> <4170@castle.ed.ac.uk> <7044@ganymede.inmos.co.uk> <BJORNL.90Jul26211631@tarpon.tds.kth.se>
Sender: 
Reply-To: geoffb@inmos.co.uk (Geoff Barrett)
Followup-To: 
Distribution: 
Organization: INMOS Limited, Bristol, UK.
Keywords: 

In article <BJORNL.90Jul26211631@tarpon.tds.kth.se> bjornl@tds.kth.se (Bj|rn Lisper) writes:
]In article <7044@ganymede.inmos.co.uk> geoffb@butcombe.inmos.co.uk (Geoff
]Barrett) writes:
]%I believe there is an Oxford DPhil thesis by a man called David Turner
]%which pointed out that strict languages are not referentially
]%transparent because of the fact that conditionals are not strict, so
]%that the equality symbol in the definition
]
]%	fun iiff (b,x,y) = if b then x else y
]
]%is not equality.  Lazy evaluation languages are referentially
]%transparent.
]
]Hmmm. How would a lazy language treat a call
]
]if(b,x,x)
]
]where the evaluation of b is non-terminating but the evaluation of x
]terminates? If "naive" left-to-right evaluation order is assumed, the
]computation will not terminate. On the other hand, it is reasonable to
]define the semantics of the if-function so that such a call returns the
]value of x. (After all, the value of x will be the answer regardless of what
]b evaluates to). What about "referential transparency" here?

I think I need a good deal of convincing that it would be "reasonable"
to define the semantics of "if(b,x,x)" to be "x".  But, even so, so long
as "iiff (b,x,x)" and "if b then x else x" evaluate to the same thing
then the language is still referentially transparent.  All I want to be
able to do is to substitute equals.

]This problem is akin to left-to-right evaluation sensitivity of goals in
]Prolog; the order of goals that are ANDed together in a Horn clause can
]affect termination even though AND, considered as a "mathematical" binary
]operation, is commutative.

One would hope that it is commutative on proper values and obeys the
same rules as the "mathematical" operator there.  The functions

   f x = 2*x; g x = x/2

satisfy the rule "f.g = id = g.f" if their domains are restricted to the
non-zero real numbers, but not on the whole of the real numbers.
Sometimes different functions do satisfy different laws.  I think this
is what the French might call a "hering rouge".

]Bjorn Lisper

						Geoff Barrett
vvv|       |vvv-------------------------------------
    \ o o /
     \   /           Wot! No Queen's Award logo?
      \_/

news@usc.edu (05/30/90)

In article <1990May28.020325.18011@comp.vuw.ac.nz> brian@comp.vuw.ac.nz (Brian Boutel) writes:

   It would be highly desirable for Miranda and Haskell to be sufficiently
   lazy to guarantee that they would never loop/die/bottom while matching
   equations if there is just one equation which matches the actual
   arguments. Unfortunately,  examples like

   . . .

   The only ways to avoid this are
   a) to outlaw programs like the example, which seem to be otherwise
   reasonable, or
   b) to go to parallel evaluation, which is expensive to simulate on a
   sequential machine.

   So, if the goal is to define a useful language for real computation, and
   to have lazy evaluation, it seems that some compromise is necessary.

um... I may be way off base here, but I seem to recall some discussion
of this at a recent APL conference (don't remember the ACM SIGAPL
volume, but it was the one in New York).  Essentially, they were using
the parallel boolean functions of the language... there is a class of
problems that serial unification does not handle correctly.. this
parallel algorithm dependended on all information being available up
front (self referent problems, such as paradoxes could not be
represented).

A big question was whether there were useful self-referent problems.
The answer to this isn't clear.

Sorry I don't remember the specific reference.. I'll try to find it
and post it.

raulmill@usc.edu

jrk@sys.uea.ac.uk (Richard Kennaway CMP RA) (06/07/90)

In article <3417@rex.cs.tulane.edu> fs@rex.cs.tulane.edu (Frank Silbermann)
writes:
>The clausal notation, which incorporates pattern-matching,
>is only a syntactic sugar.  I find pattern-matching
>a convenient shortcut when hacking, but when thinking about
>denotational semantics, I first compile such syntactic sugars
>out of my examples.

Well, I prefer not to think of them as syntactic sugar.  If you define
pattern-matching by compilation to lambda calculus, why is that better than
taking pattern-matching seriously in its own right?  Especially since what
the implementation does usually looks more like pattern-matching than lambda
calculus.  In another message, someone said that lambda calculus has a
denotational semantics, and term rewriting doesnt, but surely e.g. the
initial algebra stuff of the ADJ group contradicts the latter claim.

[quoting and paraphrasing me]
>>      Notice that f and g are nearly the same function,
>>      but they take their arguments in the opposite order.
>>      (The results differ in some cases, therefore Miranda's
>>      evaluation strategy is not quite correct.)
>
>Only if you take the syntactic sugars literally.
>
>>      ... the basic algorithm for dealing _properly_
>>      with systems like the above ...(can be found in)...  Huet & Levy
>>      "Call by need computations in non-ambiguous linear
>>      term rewriting systems", INRIA report 359, 1979.
>
>You are advocating a different kind of language.

Agreed.

>If you want to interpret a Miranda program as a
>non-ambiguous linear term rewriting system,
>you will need a way to prevent contradictions in the equations
>(which Miranda permits).  Also, I doubt that the paper you cite
>takes higher-order functions into consideration.

Another way of interpreting Miranda-style pattern-matching, studied by
Alain Laville, is to compile the ordered pattern-matching of Miranda into
a set of disjoint rules, in which the order of pattern matching does not
matter.  (He actually considered ML, but the same problems arise.)  In
other words, ordered pattern-matching is taken to be syntactic sugar, to
be defined by translating to unordered pattern-matching of conflict-free
rule-systems, which is considered meaningful in its own right.  (I admit
that this is open to the same criticism that I levelled above against
translation to lambda calculus:  one is explaining ordered p-m by
translation to unordered consistent p-m, but then just implementing
ordered p-m...)

I'm not sure what you meant above by "higher order functions".  In my f/g
example:

>>    f Nil Nil         = 1             g Nil Nil         = 1
>>    f Nil (Cons x y)  = 2             g (Cons x y) Nil  = 2
>>    f (Cons x y) z    = 3             g z (Cons x y)    = 3

if I apply f to Nil, I get a function which maps Nil to 1 and (Cons ? ?) to
2.  I can also define the argument-switching combinator:

        c x y z   =   x z y

(with which one might express one's expectation about the relation between f
and g as "c f = g") and the whole system still falls within the ambit of the
Huet-Levy paper.  Is this higher-order enough for you?

--
Richard Kennaway          SYS, University of East Anglia, Norwich, U.K.
Internet:  jrk@sys.uea.ac.uk		uucp:  ...mcvax!ukc!uea-sys!jrk

bjornl@tds.kth.se (Bj|rn Lisper) (07/27/90)

In article <7044@ganymede.inmos.co.uk> geoffb@butcombe.inmos.co.uk (Geoff
Barrett) writes:
%I believe there is an Oxford DPhil thesis by a man called David Turner
%which pointed out that strict languages are not referentially
%transparent because of the fact that conditionals are not strict, so
%that the equality symbol in the definition

%	fun iiff (b,x,y) = if b then x else y

%is not equality.  Lazy evaluation languages are referentially
%transparent.  I think this is what the original poster meant by
%"purer".

Hmmm. How would a lazy language treat a call

if(b,x,x)

where the evaluation of b is non-terminating but the evaluation of x
terminates? If "naive" left-to-right evaluation order is assumed, the
computation will not terminate. On the other hand, it is reasonable to
define the semantics of the if-function so that such a call returns the
value of x. (After all, the value of x will be the answer regardless of what
b evaluates to). What about "referential transparency" here?

This problem is akin to left-to-right evaluation sensitivity of goals in
Prolog; the order of goals that are ANDed together in a Horn clause can
affect termination even though AND, considered as a "mathematical" binary
operation, is commutative.

Bjorn Lisper