[m-dev.] for review: lazy evaluation [round 2]

Fergus Henderson fjh at cs.mu.OZ.AU
Mon Mar 15 19:56:15 AEDT 1999


On 13-Mar-1999, Fergus Henderson <fjh at cs.mu.OZ.AU> wrote:
> Clearly my original proposal didn't generate enough consensus
> for it to belong in the standard library.  So this time
> I'm putting it in extras.  I've also included some examples
> and added a lot of documentation.

Here's what I will commit.  The two README file changes are new.
I've also made some minor changes to the documentation
in lazy.m; relative diff included below.
I'm going to commit these without waiting for review,
since I think the additional documentation can hardly
harm things, but review comments on the READMEs would
still be appreciated.

Estimated hours taken: 16

extras/README:
extras/lazy_evaluation/README:
	Document the stuff in this new directory.

extras/lazy_evaluation/lazy.m:
	New module for lazy evaluation.

extras/lazy_evaluation/lazy_list.m:
extras/lazy_evaluation/lazy_list_test.m:
	A demo of using lazy lists.

diff -u backup/lazy.m ./lazy.m
--- backup/lazy.m	Sat Mar 13 21:01:39 1999
+++ ./lazy.m	Mon Mar 15 18:40:51 1999
@@ -20,26 +20,32 @@
 % which uses lazy(T) for the recursion.  See the module
 % `lazy_list.m' for an example of how to do this.
 %
+% See the file README in this directory
+%
 %-----------------------------------------------------------------------------%
 
 :- module lazy.
 :- interface.
 
-% a lazy(T) is a value of type T which will only be evaluated on demand.
+% A lazy(T) is a value of type T which will only be evaluated on demand.
 :- type lazy(T).
 
 % :- inst lazy(I).  % abstract
 :- inst lazy == lazy(ground).
 
-% convert a value from type T to lazy(T)
+% Convert a value from type T to lazy(T)
 :- func val(T) = lazy(T).
 :- mode val(in) = out(lazy) is det.
 
-% construct a lazily-evaluated lazy(T) from a closure
+% Construct a lazily-evaluated lazy(T) from a closure
 :- func delay((func) = T) = lazy(T).
 :- mode delay((func) = out is det) = out(lazy) is det.
 
-% force the evaluation of a lazy(T), and return the result as type T.
+% Force the evaluation of a lazy(T), and return the result as type T.
+% Note that if the type T may itself contains subterms of type lazy(T),
+% as is the case when T is a recursive type like the lazy_list(T) type
+% defined in lazy_list.m, those subterms will not be evaluated --
+% force/1 only forces evaluation of the lazy/1 term at the top level.
 :- func force(lazy(T)) = T.
 :- mode force(in(lazy)) = out is det.
 
@@ -144,7 +150,7 @@
 :-        mode update_closure(in, in) is det.
 
 % Note that the implementation of this impure predicate relies on
-% some implementation details of the Mercury implementation.
+% some details of the Mercury implementation.
 :- pragma c_code(
 	update_closure(MercuryTerm::in, NewValue::in),
 	[will_not_call_mercury],
Index: extras/README
===================================================================
RCS file: /home/mercury1/repository/mercury/extras/README,v
retrieving revision 1.4
diff -u -r1.4 README
--- README	1998/12/07 01:55:01	1.4
+++ README	1999/03/15 04:31:42
@@ -23,6 +23,12 @@
 		and GUIs in Mercury: a Mercury interface to tcl/tk
 		and a Mercury binding to OpenGL.
 
+lazy_evaluation
+		A library module `lazy' containing support for optional
+		lazy evaluation in Mercury, together with some examples
+		of its use, including a module `lazy_list' that defines
+		a lazy list data type.
+
 odbc		A Mercury interface to ODBC (Open Database Connectivity),
 		for interfacing to standard relational database packages.
 
Index: extras/lazy_evaluation/README
===================================================================
RCS file: README
diff -N README
--- /dev/null	Mon Mar 15 19:46:19 1999
+++ README	Mon Mar 15 16:51:11 1999
@@ -0,0 +1,71 @@
+This directory contains support for optional lazy evaluation.
+Using the modules defined here, you can write Mercury code that
+makes use of lazily evaluated data structures. 
+
+Our implementation of lazy evaluation requires you to use a different
+type, lazy(T), whenever you want things to be lazily evaluated, and
+requires you to insert explicit calls to delay/1 or force/1 whenever
+lazy evaluation requires the creation or evaluation of closures.
+
+This directory contains the following files:
+
+	lazy.m:
+		This module defines the lazy(T) type, and the force/1
+		and delay/1 functions.
+
+	lazy_list.m:
+		This module defines a type lazy_list(T) using the lazy(T) type,
+		and also defines a few functions and predicates that operate
+		on lazy lists.
+
+	lazy_list_test.m:
+		This is just a very simple example showing the use of lazy
+		lists.
+
+In comparison with lazy functional languages, the disadvantage of our
+approach is that inserting the lazy(T) types and the explicit calls to
+force/1 and delay/1 requires additional work when you are writing your
+code.  Fortunately the Mercury compiler's static type checking will
+ensure that the calls to force/1 and delay/1 are consistent with the
+use of lazy(T) types.  But even so, putting all the calls to force/1
+and delay/1 in the right place can still be rather tedious. 
+
+In return, however, we get several important advantages.
+
+The first is that there are absolutely no efficiency costs resulting
+from lazy evaluation if you don't use it.  This is in contrast to many
+implementations of lazy functional languages, where you often pay a
+significant efficiency cost simply because things *might* be lazy, even
+when in actual fact they are not.  Compilers for lazy functional
+languages often try to avoid these costs by performing strictness
+analysis, but current compilers can only infer strictness of functions,
+not data types; using lazy data types rather than strict data types can
+have a very large impact on efficiency (e.g. a factor of 5).  Also, in
+the presence of separate compilation, compilers may need to make
+conservative assumptions about strictness.
+
+The second advantage is that the creation and evaluation of closures is
+explicit in the source code, which makes it much easier to reason about
+the performance of your programs.  Programs in languages where laziness
+is the default often suffer from space leaks or unexpectedly high
+memory usage, and these problems can be _extremely_ difficult to track
+down and understand, even for very experienced programmers.
+
+The third advantage is that supporting lazy evaluation via a library
+module keeps the language and its semantics simple.  We're not really
+providing lazy evaluation per se, we just _emulating_ it by passing
+lambda expressions as arguments.  So the "Semantics" chapter of the
+language reference manual does not need to be modified at all.
+Supporting lazy evaluation via a library module also keeps the
+implementation simple -- the module lazy.m requires only a very
+small amount of implementation-dependent code, and none of the
+rest of the implementation need change.
+
+Our current implementation of lazy evaluation is not very efficient.
+There are several for this.  One is that promise_only_solution/1,
+which is used in the implementation of force/1, is currently implemented
+rather inefficiently.  Another is that the lazy(T) type currently uses
+two levels of indirection, whereas it really ought to use only one.
+Finally, for maximum efficiency, we would need to inline delay/1,
+but that is not possible in the current implementation.  Solving these
+latter two issues would require a bit more compiler support.
Index: extras/lazy_evaluation/lazy.m
===================================================================
RCS file: lazy.m
diff -N lazy.m
--- /dev/null	Mon Mar 15 19:46:19 1999
+++ lazy.m	Mon Mar 15 18:40:51 1999
@@ -0,0 +1,164 @@
+%-----------------------------------------------------------------------------%
+% Copyright (C) 1999 The University of Melbourne.
+% This file may only be copied under the terms of the GNU Library General
+% Public License - see the file COPYING.LIB in the Mercury distribution.
+%-----------------------------------------------------------------------------%
+%
+% lazy.m - provides support for optional lazy evaluation.
+%
+% Author: fjh.
+% Stability: medium.
+%
+% This module provides the data type `lazy(T)' and the functions
+% `val', `delay', and `force', which can be used to emulate lazy
+% evaluation.
+%
+% Note that laziness is most useful for recursive data types,
+% and for that case, you can't just use e.g. `lazy(list(T))',
+% since that would only be lazy at the top-level; instead
+% you need to define a new recursive data type lazy_list(T)
+% which uses lazy(T) for the recursion.  See the module
+% `lazy_list.m' for an example of how to do this.
+%
+% See the file README in this directory for additional documentation.
+%
+%-----------------------------------------------------------------------------%
+
+:- module lazy.
+:- interface.
+
+% A lazy(T) is a value of type T which will only be evaluated on demand.
+:- type lazy(T).
+
+% :- inst lazy(I).  % abstract
+:- inst lazy == lazy(ground).
+
+% Convert a value from type T to lazy(T)
+:- func val(T) = lazy(T).
+:- mode val(in) = out(lazy) is det.
+
+% Construct a lazily-evaluated lazy(T) from a closure
+:- func delay((func) = T) = lazy(T).
+:- mode delay((func) = out is det) = out(lazy) is det.
+
+% Force the evaluation of a lazy(T), and return the result as type T.
+% Note that if the type T may itself contains subterms of type lazy(T),
+% as is the case when T is a recursive type like the lazy_list(T) type
+% defined in lazy_list.m, those subterms will not be evaluated --
+% force/1 only forces evaluation of the lazy/1 term at the top level.
+:- func force(lazy(T)) = T.
+:- mode force(in(lazy)) = out is det.
+
+%
+% The declarative semantics of the above constructs are given by the
+% following equations:
+%
+%	val(X) = delay((func) = X).
+%
+%	force(delay(F)) = apply(F).
+%
+% The operational semantics satisfy the following:
+%
+% - val/1 and delay/1 both take O(1) time and use O(1) additional space.
+%   In particular, delay/1 does not evaluate its argument using apply/1.
+%
+% - When force/1 is first called for a given term, it uses apply/1 to
+%   evaluate the term, and then saves the result computed by destructively
+%   modifying its argument; subsequent calls to force/1 on the same term
+%   will return the same result.  So the time to evaluate force(X), where
+%   X = delay(F), is O(the time to evaluate apply(F)) for the first call,
+%   and O(1) time for subsequent calls.
+%
+% - Equality on values of type lazy(T) is implemented by calling force/1
+%   on both arguments and comparing the results.  So if X and Y have type
+%   lazy(T), and both X and Y are ground, then the time to evaluate X = Y
+%   is O(the time to evaluate (X1 = force(X)) + the time to evaluate
+%   (Y1 = force(Y)) + the time to unify X1 and Y1).
+%
+% -----------------------------------------------------------------------------%
+
+% The following may be needed occaisionally, in case
+% the compiler can't infer the right higher-order inst...
+% It just returns its argument, cast to the correct inst.
+:- func inst_cast(lazy(T)) = lazy(T).
+:- mode inst_cast(in) = out(lazy) is det.
+
+%-----------------------------------------------------------------------------%
+
+:- implementation.
+:- interface.
+
+% implementation details
+:- inst lazy(I) ---> value(I) ; closure((func) = out(I) is det).
+
+:- implementation.
+
+% Note that we use a user-defined equality predicate to ensure
+% that unifying two lazy(T) values will do the right thing.
+:- type lazy(T) ---> value(T) ; closure((func) = T)
+	where equality is equal_values.
+
+:- pred equal_values(lazy(T), lazy(T)).
+:- mode equal_values(in, in) is semidet.
+equal_values(X, Y) :-
+	force(inst_cast(X)) = force(inst_cast(Y)).
+
+:- pragma c_code(inst_cast(F::in) = (F2::out(lazy)),
+	[will_not_call_mercury, thread_safe], "F2 = F;").
+
+%-----------------------------------------------------------------------------%
+
+val(X) = value(X).
+delay(F) = closure(F).
+
+% If the compiler were to evaluate calls to delay/1 at compile time,
+% it could put the resulting closure/1 term in read-only memory,
+% which would make destructively updating it rather dangerous.
+% So we'd better not let the compiler inline delay/1.
+:- pragma no_inline(delay/1).
+
+%-----------------------------------------------------------------------------%
+
+% The call to promise_only_solution is needed to tell the
+% compiler that force will return equal answers given
+% arguments that are equal but that have different representations.
+force(Lazy) = promise_only_solution(do_force(Lazy)).
+
+:- pred do_force(lazy(T), T).
+:- mode do_force(in(lazy), out) is cc_multi.
+
+% The pragma promise_pure is needed to tell the compiler that
+% do_force is pure, even though it calls impure code.
+:- pragma promise_pure(do_force/2).
+
+do_force(Lazy, Value) :-
+	(	
+		Lazy = value(Value)
+	;
+		Lazy = closure(Func),
+		Value = apply(Func),
+
+		% Destructively update the closure with a new
+		% closure that immediately returns the same value,
+		% to avoid having to recompute the same result
+		% next time.
+		NewFunc = ((func) = Result :- Result = Value),
+		impure update_closure(Lazy, NewFunc)
+	).
+
+:- impure pred update_closure(T1, T2).
+:-        mode update_closure(in, in) is det.
+
+% Note that the implementation of this impure predicate relies on
+% some details of the Mercury implementation.
+:- pragma c_code(
+	update_closure(MercuryTerm::in, NewValue::in),
+	[will_not_call_mercury],
+"{
+	/* strip off tag bits */
+	Word *ptr = (Word *) strip_tag(MercuryTerm);
+	/* destructively update value */
+	*ptr = NewValue;
+}").
+
+%-----------------------------------------------------------------------------%
Index: extras/lazy_evaluation/lazy_list.m
===================================================================
RCS file: lazy_list.m
diff -N lazy_list.m
--- /dev/null	Mon Mar 15 19:46:19 1999
+++ lazy_list.m	Sat Mar 13 20:19:06 1999
@@ -0,0 +1,131 @@
+%-----------------------------------------------------------------------------%
+%
+% This is an example of how to use the `lazy' module to define
+% a recursive lazy data type, in this case lazy lists.
+% It also defines a small number of functions and predicates
+% that operate on lazy lists.
+%
+% See also lazy_list_test.m, which is an example program using this module.
+%
+% This source file is hereby placed in the public domain.  -fjh (the author).
+
+:- module lazy_list.
+:- interface.
+:- import_module lazy, int, list.
+
+%-----------------------------------------------------------------------------%
+
+	% The definition of the type `lazy_list(T)':
+	%	A lazy lazy_list is either an empty lazy_list, denoted `[]',
+	%	or an element `Head' of type `T' followed by a lazily
+	%	evaluated tail `Tail', of type `lazy(lazy_list(T))',
+	%	denoted `[Head | Tail]'.
+
+:- type lazy_list(T) ---> [] ; [T | lazy(lazy_list(T))].
+
+:- inst lazy_list(I) ---> [] ; [I | lazy(lazy_list(I))].
+:- inst lazy_list == lazy_list(ground).
+
+:- inst nonempty_lazy_list(I) ---> [I | lazy(lazy_list(I))].
+:- inst nonempty_lazy_list == nonempty_lazy_list(ground).
+
+%-----------------------------------------------------------------------------%
+
+	% force evaluation of (the top level of) a lazy list
+:- func force_list(lazy(lazy_list(T))) = lazy_list(T).
+:- mode force_list(in(lazy(lazy_list))) = out(lazy_list) is det.
+
+%-----------------------------------------------------------------------------%
+
+	% Convert a lazy_list to an ordinary list.
+:- func to_list(lazy_list(T)) = list(T).
+:- mode to_list(in(lazy_list)) = out is det.
+
+	% Convert an ordinary list to a lazy_list.
+:- func from_list(list(T)) = lazy_list(T).
+:- mode from_list(in) = out(lazy_list) is det.
+
+%-----------------------------------------------------------------------------%
+
+	% A lazy_list function version of the usual append predicate:
+	% append(Start, End) = List is true iff
+	% `List' is the result of concatenating `Start' and `End'.
+	%
+:- func append(lazy_list(T), lazy(lazy_list(T))) = lazy_list(T).
+:- mode append(in(lazy_list), in(lazy(lazy_list))) = out(lazy_list)
+	is det.
+
+	% member(Elem, List) :
+	%	True iff `List' contains `Elem'.
+:- pred member(T, lazy_list(T)).
+:- mode member(in, in(lazy_list)) is semidet.
+:- mode member(out, in(nonempty_lazy_list)) is multi.
+:- mode member(out, in(lazy_list)) is nondet.
+
+%-----------------------------------------------------------------------------%
+
+	% iterate(F, X0) = [X0, F(X0), F(F(X0)), F(F(F(X0))), ...]
+:- func iterate(func(T) = T, T) = lazy_list(T).
+:- mode iterate(func(in) = out is det, in) = out(lazy_list) is det.
+
+	% take(N, L) returns the first N elements of L
+:- func take(int, lazy_list(T)) = lazy_list(T).
+:- mode take(in, in(lazy_list)) = out(lazy_list) is det.
+
+	% map(F, [X0, X1, X2, ...]) = [F(X0), F(X1), F(X2), ...].
+:- func map(func(X) = Y, lazy_list(X)) = lazy_list(Y).
+:- mode map(func(in) = out is det, in(lazy_list)) = out(lazy_list) is det.
+
+%-----------------------------------------------------------------------------%
+%-----------------------------------------------------------------------------%
+
+:- implementation.
+
+force_list(Xs) = list_inst_cast(force(Xs)).
+
+% Because the Mercury mode system is not properly polymorphic,
+% it doesn't always infer the right inst.  We sometimes need
+% to use inst casts (which can be implemented using `pragma c_code').
+% :-(
+
+:- func list_inst_cast(lazy_list(T)) = lazy_list(T).
+:- mode list_inst_cast(in) = out(lazy_list) is det.
+
+:- pragma c_code(list_inst_cast(F::in) = (F2::out(lazy_list)),
+	[will_not_call_mercury, thread_safe], "F2 = F;").
+
+%-----------------------------------------------------------------------------%
+
+to_list([]) = [].
+to_list([X | Xs]) = [X | to_list(force_list(Xs))].
+
+from_list([]) = [].
+from_list([X | Xs]) =
+	[X | delay((func) = R :- R = from_list(Xs))].
+
+%-----------------------------------------------------------------------------%
+
+append([], Ys) = force_list(Ys).
+append([X | Xs], Ys) =
+	[X | delay((func) = R :- R = append(force_list(Xs), Ys))].
+
+member(X, [X | _]).
+member(X, [_ | Xs]) :-
+	member(X, force_list(Xs)).
+
+%-----------------------------------------------------------------------------%
+
+map(_, []) = [].
+map(F, [H|T]) = [F(H) | delay((func) = R :- R = map(F, force_list(T)))].
+
+iterate(F, X0) = [X0 | delay((func) = R :- R = iterate(F, F(X0)))].
+
+take(_, []) = [].
+take(N, [X|Xs]) =
+	(if N > 0 then
+		[X | delay((func) = R :- R = take(N-1, force_list(Xs)))]
+	else
+		[]
+	).
+
+%-----------------------------------------------------------------------------%
Index: extras/lazy_evaluation/lazy_list_test.m
===================================================================
RCS file: lazy_list_test.m
diff -N lazy_list_test.m
--- /dev/null	Mon Mar 15 19:46:19 1999
+++ lazy_list_test.m	Sat Mar 13 20:15:41 1999
@@ -0,0 +1,27 @@
+%-----------------------------------------------------------------------------%
+%
+% lazy_list_test.m:
+%	This is a trivial example of the use of lazy lists.
+%
+% This source file is hereby placed in the public domain.  -fjh (the author).
+
+:- module lazy_list_test.
+:- interface.
+:- import_module io.
+
+:- pred main(io__state::di, io__state::uo) is det.
+
+%-----------------------------------------------------------------------------%
+
+:- implementation.
+:- import_module lazy, lazy_list, int.
+
+:- func double(int) = int.
+double(X) = 2 * X.
+
+main -->
+	{ L = iterate(double, 1) },	% construct an infinite list...
+	{ L10 = take(10, L) },		% extract the first 10 elements
+	print(to_list(L10)), nl.	% print them
+
+%-----------------------------------------------------------------------------%
-- 
Fergus Henderson <fjh at cs.mu.oz.au>  |  "I have always known that the pursuit
WWW: <http://www.cs.mu.oz.au/~fjh>  |  of excellence is a lethal habit"
PGP: finger fjh at 128.250.37.3        |     -- the last words of T. S. Garp.



More information about the developers mailing list