Table of Contents
rlm@2:-
rlm@2:
- 1 Abstract rlm@2:
- 2 Introduction
rlm@2:
-
rlm@2:
- 2.1 Simple Loops
rlm@2:
-
rlm@2:
- 2.1.1 Simple Recursion rlm@2:
- 2.1.2 Iteration rlm@2:
rlm@2:
rlm@2: - 2.1 Simple Loops
rlm@2:
- 3 Imperative Programming
rlm@2:
-
rlm@2:
- 3.1 Compound Statements rlm@2:
- 3.2 2.2. The G0 TO Statement rlm@2:
- 3.3 2.3. Simple Assignment rlm@2:
- 3.4 Compound Expressions ' rlm@2:
rlm@2: - 4 Continuations
rlm@2:
-
rlm@2:
- 4.1 Continuation-Passing Recursion rlm@2:
- 4.2 Escape Expressions rlm@2:
rlm@2:
1 Abstract
rlm@2:rlm@2: We demonstrate how to model the following common programming constructs in rlm@2: terms of an applicative order language similar to LISP: rlm@2:
-
rlm@2:
- Simple Recursion rlm@2: rlm@2:
- Iteration rlm@2: rlm@2:
- Compound Statements and Expressions rlm@2: rlm@2:
- GO TO and Assignment rlm@2: rlm@2:
- Continuation—Passing rlm@2: rlm@2:
- Escape Expressions rlm@2: rlm@2:
- Fluid Variables rlm@2: rlm@2:
- Call by Name, Call by Need, and Call by Reference rlm@2: rlm@2:
The models require only (possibly self-referent) lambda application, rlm@2: conditionals, and (rarely) assignment. No complex data structures such as rlm@2: stacks are used. The models are transparent, involving only local syntactic rlm@2: transformations. rlm@2: Some of these models, such as those for GO TO and assignment, are already well rlm@2: known, and appear in the work of Landin, Reynolds, and others. The models for rlm@2: escape expressions, fluid variables, and call by need with side effects are rlm@2: new. This paper is partly tutorial in intent, gathering all the models rlm@2: together for purposes of context. rlm@2: This report describes research done at the Artificial Intelligence Laboratory rlm@2: of the Massachusetts Institute of Teehnology. Support for the laboratory's rlm@2: artificial intelligence research is provided in part by the Advanced Research rlm@2: Projects Agency of the Department of Defense under Office of Naval Research rlm@2: contract N000l4-75-C-0643. rlm@2:
rlm@2:2 Introduction
rlm@2:rlm@2: We catalogue a number of common programming constructs. For each rlm@2: construct we examine "typical" usage in well-known programming languages, and rlm@2: then capture the essence of the semantics of the construct in terms of a rlm@2: common meta-language. rlm@2: The lambda calculus {Note Alonzowins} is often used as such a meta- rlm@2: language. Lambda calculus offers clean semantics, but it is clumsy because it rlm@2: was designed to be a minimal language rather than a convenient one. All rlm@2: lambda calculus "functions" must take exactly one "argument"; the only "data rlm@2: type" is lambda expressions; and the only "primitive operation‘ is variable‘ rlm@2: substitution. While its utter simplicity makes lambda calculus ideal for rlm@2: logicians, it is too primitive for use by programmers. The meta-language we rlm@2: use is a programming language called SCHEME {Note Schemepaper) which is based rlm@2: on lambda calculus. rlm@2: SCHEME is a dialect of LISP. [McCarthy 62] It is an expression- rlm@2: oriented, applicative order, interpreter-based language which allows one to rlm@2: manipulate programs as data. It differs from most current dialects of LISP in rlm@2: that it closes all lambda expressions in the environment of their definition rlm@2: or declaration, rather than in the execution environment. {Note Closures} rlm@2: This preserves the substitution semantics of lambda calculus, and has the rlm@2: consequence that all variables are lexically scoped, as in ALGOL. [Naur 63] rlm@2: Another difference is that SCHEME is implemented in such a way that tail- rlm@2: recursions execute without net growth of the interpreter stack. {Note rlm@2: Schemenote} We have chosen to use LISP syntax rather than, say, ALGOL syntax rlm@2: because we want to treat programs as data for the purpose of describing rlm@2: transformations on the code. LISP supplies names for the parts of an rlm@2: executable expression and standard operators for constructing expressions and rlm@2: extracting their components. The use of LISP syntax makes the structure of rlm@2: such expressions manifest. We use ALGOL as an expository language, because it rlm@2: is familiar to many people, but ALGOL is not sufficiently powerful to express rlm@2: the necessary concepts; in particular, it does not allow functions to return rlm@2: functions as values. we are thus forced to use a dialect of LISP in many rlm@2: cases. rlm@2: We will consider various complex programming language constructs and rlm@2: show how to model them in terms of only a few simple ones. As far as possible rlm@2: we will use only three control constructs from SCHEME: LAMBDA expressions, as rlm@2: in LISP, which are Just functions with lexically scoped free variables; rlm@2: LABELS, which allows declaration of mutually recursive procedures (Note rlm@2: Labelsdef}; and IF, a primitive conditional expression. For more complex rlm@2: modelling we will introduce an assignment primitive (ASET).i We will freely rlm@2: assume the existence of other comon primitives, such as arithmetic functions. rlm@2: The constructs we will examine are divided into four broad classes. rlm@2: The first is sfinph?Lo0pl; this contains simple recursions and iterations, and rlm@2: an introduction to the notion of continuations. The second is hmponflivo rlm@2: Connrucls; this includes compound statements, G0 T0, and simple variable rlm@2: assignments. The third is continuations, which encompasses the distinction between statements and expressions, escape operators (such as Landin's J- rlm@2: operator [Landin 65] and Reynold's escape expression [Reynolds 72]). and fluid rlm@2: (dynamically bound) variables. The fourth is Parameter Passing Mechanisms, such rlm@2: as ALGOL call—by-name and FORTRAN call-by-location. rlm@2: Some of the models presented here are already well-known, particularly rlm@2: those for G0 T0 and assignment. [McCarthy 60] [Landin 65] [Reynolds 72] rlm@2: Those for escape operators, fluid variables, and call-by-need with side rlm@2: effects are new. rlm@2:
rlm@2:2.1 Simple Loops
rlm@2:By simple loops we mean constructs which enable programs to execute the rlm@2: same piece of code repeatedly in a controlled manner. Variables may be made rlm@2: to take on different values during each repetition, and the number of rlm@2: repetitions may depend on data given to the program. rlm@2:
rlm@2:2.1.1 Simple Recursion
rlm@2:One of the easiest ways to produce a looping control structure is to rlm@2: use a recursive function, one which calls itself to perform a subcomputation. rlm@2: For example, the familiar factorial function may be written recursively in rlm@2: ALGOL: ' rlm@2:
rlm@2: rlm@2: rlm@2:integer procedure fact(n) value n: integer n: rlm@2: fact := if n=0 then 1 else n*fact(n-1); rlm@2: rlm@2:rlm@2: rlm@2: rlm@2: rlm@2: rlm@2:
rlm@2: The invocation fact(n)
computes the product of the integers from 1 to n using
rlm@2: the identity n!=n(n-1)! (n>0). If \(n\) is zero, 1 is returned; otherwise fact
:
rlm@2: calls itself recursively to compute \((n-1)!\) , then multiplies the result by \(n\)
rlm@2: and returns it.
rlm@2:
rlm@2: This same function may be written in Clojure as follows: rlm@2:
rlm@2: rlm@2: rlm@2:(ns categorical.imperative) rlm@2: (defn fact[n] rlm@2: (if (= n 0) 1 (* n (fact (dec n)))) rlm@2: ) rlm@2: rlm@2:rlm@2: rlm@2: rlm@2: rlm@2: rlm@2: rlm@2:
rlm@2: Clojure does not require an assignment to the ``variable'' fan: to return a value rlm@2: as ALGOL does. The IF primitive is the ALGOL if-then-else rendered in LISP rlm@2: syntax. Note that the arithmetic primitives are prefix operators in SCHEME. rlm@2:
rlm@2:2.1.2 Iteration
rlm@2:There are many other ways to compute factorial. One important way is rlm@2: through the use of iteration. rlm@2: A comon iterative construct is the DO loop. The most general form we rlm@2: have seen in any programming language is the MacLISP DO [Moon 74]. It permits rlm@2: the simultaneous initialization of any number of control variables and the rlm@2: simultaneous stepping of these variables by arbitrary functions at each rlm@2: iteration step. The loop is terminated by an arbitrary predicate, and an rlm@2: arbitrary value may be returned. The DO loop may have a body, a series of rlm@2: expressions executed for effect on each iteration. A version of the MacLISP rlm@2: DO construct has been adopted in SCHEME. rlm@2:
rlm@2:rlm@2: The general form of a SCHEME DO is: rlm@2:
rlm@2: rlm@2: rlm@2:(DO ((<var1> (init1> <stepl>) rlm@2: ((var2> <init2> <step2>) rlm@2: (<varn> <intn> (stepn>)) rlm@2: (<pred> (value>) rlm@2: (optional body>) rlm@2: rlm@2:rlm@2: rlm@2: rlm@2: rlm@2:
rlm@2: The semantics of this are that the variables are bound and initialized to the rlm@2: values of the <initi> expressions, which must all be evaluated in the rlm@2: environment outside the D0; then the predicate <pred> is evaluated in the new rlm@2: environment, and if TRUE, the (value) is evaluated and returned. Otherwise rlm@2: the (optional body) is evaluated, then each of the steppers <stepi> is rlm@2: evaluated in the current environment, all the variables made to have the rlm@2: results as their values, the predicate evaluated again, and so on. rlm@2: Using D0 loops in both ALGOL and SCHEME, we may express FACT by means rlm@2: of iteration. rlm@2:
rlm@2: rlm@2: rlm@2:integer procedure fact(n); value n; integer n: rlm@2: begin rlm@2: integer m, ans; rlm@2: ans := 1; rlm@2: for m := n step -l until 0 do ans := m*ans; rlm@2: fact := ans; rlm@2: end; rlm@2: rlm@2:rlm@2: rlm@2: rlm@2: rlm@2: rlm@2: rlm@2:
(in-ns 'categorical.imperative) rlm@2: (defn fact-do[n] rlm@2: ) rlm@2: rlm@2:rlm@2: rlm@2: rlm@2: rlm@2: rlm@2:
rlm@2: Note that the SCHEME D0 loop in FACT has no body – the stepping functions do rlm@2: all the work. The ALGOL DO loop has an assignment in its body; because an rlm@2: ALGOL DO loop can step only one variable, we need the assignment to step the rlm@2: the variable "manually'. rlm@2: In reality the SCHEME DO construct is not a primitive; it is a macro rlm@2: which expands into a function which performs the iteration by tail—recursion. rlm@2: Consider the following definition of FACT in SCHEME. Although it appears to rlm@2: be recursive, since it "calls itself", it is entirely equivalent to the DO rlm@2: loop given above, for it is the code that the DO macro expands into! It rlm@2: captures the essence of our intuitive notion of iteration, because execution rlm@2: of this program will not produce internal structures (e.g. stacks or variable rlm@2: bindings) which increase in size with the number of iteration steps. rlm@2:
rlm@2: rlm@2: rlm@2: rlm@2:(in-ns 'categorical.imperative) rlm@2: (defn fact-do-expand[n] rlm@2: (let [fact1 rlm@2: (fn[m ans] rlm@2: (if (zero? m) ans rlm@2: (recur (dec m) (* m ans))))] rlm@2: (fact1 n 1))) rlm@2: rlm@2:rlm@2: rlm@2: rlm@2: rlm@2: rlm@2:
rlm@2: From this we can infer a general way to express iterations in SCHEME in rlm@2: a manner isomorphic to the HacLISP D0. The expansion of the general D0 loop rlm@2:
rlm@2: rlm@2: rlm@2:(DO ((<varl> <init1> <step1>) rlm@2: (<var2> (init2) <step2>) rlm@2: ... rlm@2: (<varn> <initn> <stepn>)) rlm@2: (<pred> <va1ue>) rlm@2: <body>) rlm@2: rlm@2:rlm@2: rlm@2: rlm@2: rlm@2:
rlm@2: is this: rlm@2:
rlm@2: rlm@2: rlm@2:(let [doloop rlm@2: (fn [dummy <var1> <var2> ... <varn>) rlm@2: (if <pred> <value> rlm@2: (recur <body> <step1> <step2> ... <stepn>)))] rlm@2: rlm@2: (doloop nil <init1> <init2> ... <initn>)) rlm@2: rlm@2:rlm@2: rlm@2: rlm@2: rlm@2:
rlm@2: The identifiers doloop
and dummy
are chosen so as not to conflict with any
rlm@2: other identifiers in the program.
rlm@2: Note that, unlike most implementations of D0, there are no side effects
rlm@2: in the steppings of the iteration variables. D0 loops are usually modelled
rlm@2: using assignment statements. For example:
rlm@2:
for r :1 0 step b until c do <statement>; rlm@2: rlm@2:rlm@2: rlm@2: rlm@2: rlm@2: rlm@2:
rlm@2: can be modelled as follows: [Naur 63] rlm@2:
rlm@2: rlm@2: rlm@2:begin rlm@2: x := a; rlm@2: L: if (x-c)*sign(n) > 0 then go to Endloop; rlm@2: <statement>; rlm@2: x := x+b; rlm@2: go to L: rlm@2: Endloop: rlm@2: end; rlm@2: rlm@2:rlm@2: rlm@2: rlm@2: rlm@2:
rlm@2: Later we will see how such assignment statements can in general be rlm@2: modelled without using side effects. rlm@2:
rlm@2:3 Imperative Programming
rlm@2:Lambda calculus (and related languages, such as ``pure LISP'') is often rlm@2: used for modelling the applicative constructs of programming languages. rlm@2: However, they are generally thought of as inappropriate for modelling rlm@2: imperative constructs. In this section we show how imperative constructs may rlm@2: be modelled by applicative SCHEME constructs. rlm@2:
rlm@2:3.1 Compound Statements
rlm@2:The simplest kind of imperative construct is the statement sequencer, rlm@2: for example the compound statement in ALGOL: rlm@2:
rlm@2: rlm@2: rlm@2:begin rlm@2: S1; rlm@2: S2; rlm@2: end rlm@2: rlm@2:rlm@2: rlm@2: rlm@2: rlm@2: rlm@2:
rlm@2: This construct has two interesting properties: rlm@2:
-
rlm@2:
- (1) It performs statement S1 before S2, and so may be used for rlm@2: sequencing. rlm@2: rlm@2:
- (2) S1 is useful only for its side effects. (In ALGOL, S2 must also rlm@2: be a statement, and so is also useful only for side effects, but rlm@2: other languages have compound expressions containing a statement rlm@2: followed by an expression.) rlm@2: rlm@2:
rlm@2: The ALGOL compound statement may actually contain any number of statements, rlm@2: but such statements can be expressed as a series of nested two-statement rlm@2: compounds. That is: rlm@2:
rlm@2: rlm@2: rlm@2:begin rlm@2: S1; rlm@2: S2; rlm@2: ... rlm@2: Sn-1; rlm@2: Sn; rlm@2: end rlm@2: rlm@2:rlm@2: rlm@2: rlm@2: rlm@2:
rlm@2: is equivalent to: rlm@2:
rlm@2: rlm@2: rlm@2: rlm@2:begin rlm@2: S1; rlm@2: begin rlm@2: S2; rlm@2: begin rlm@2: ... rlm@2: rlm@2: begin rlm@2: Sn-1; rlm@2: Sn; rlm@2: end; rlm@2: end; rlm@2: end: rlm@2: end rlm@2: rlm@2:rlm@2: rlm@2: rlm@2: rlm@2: rlm@2:
rlm@2: It is not immediately apparent that this sequencing can be expressed in a rlm@2: purely applicative language. We can, however, take advantage of the implicit rlm@2: sequencing of applicative order evaluation. Thus, for example, we may write a rlm@2: two-statement sequence as follows: rlm@2:
rlm@2: rlm@2: rlm@2: rlm@2:((fn[dummy] S2) S1)
rlm@2:
rlm@2:
rlm@2:
rlm@2:
rlm@2:
rlm@2:
rlm@2:
rlm@2: where dummy
is an identifier not used in S2. From this it is
rlm@2: manifest that the value of S1 is ignored, and so is useful only for
rlm@2: side effects. (Note that we did not claim that S1 is expressed in a
rlm@2: purely applicative language, but only that the sequencing can be so
rlm@2: expressed.) From now on we will use the form (BLOCK S1 S2)
as an
rlm@2: abbreviation for this expression, and (BLOCK S1 S2...Sn-1 Sn)
as an
rlm@2: abbreviation for
rlm@2:
(BLOCK S1 (BLOCK S2 (BLOCK ... (BLOCK Sn-1 Sn)...))) rlm@2: rlm@2:rlm@2: rlm@2: rlm@2: rlm@2: rlm@2:
3.2 2.2. The G0 TO Statement
rlm@2:rlm@2: A more general imperative structure is the compound statement with rlm@2: labels and G0 T0s. Consider the following code fragment due to rlm@2: Jacopini, taken from Knuth: [Knuth 74] rlm@2:
rlm@2: rlm@2: rlm@2:begin rlm@2: L1: if B1 then go to L2; S1; rlm@2: if B2 then go to L2; S2; rlm@2: go to L1; rlm@2: L2: S3; rlm@2: rlm@2:rlm@2: rlm@2: rlm@2: rlm@2: rlm@2:
rlm@2: It is perhaps surprising that this piece of code can be syntactically rlm@2: transformed into a purely applicative style. For example, in SCHEME we rlm@2: could write: rlm@2:
rlm@2: rlm@2: rlm@2: rlm@2:(in-ns 'categorical.imperative) rlm@2: (let rlm@2: [L1 (fn[] rlm@2: (if B1 (L2) (do S1 rlm@2: (if B2 (L2) (do S2 (L1)))))) rlm@2: L2 (fn[] S3)] rlm@2: (L1)) rlm@2: rlm@2:rlm@2: rlm@2: rlm@2: rlm@2: rlm@2:
rlm@2: As with the D0 loop, this transformation depends critically on SCHEME's rlm@2: treatment of tail-recursion and on lexical scoping of variables. The labels rlm@2: are names of functions of no arguments. In order to ‘go to" the labeled code, rlm@2: we merely call the function named by that label. rlm@2:
rlm@2:3.3 2.3. Simple Assignment
rlm@2:Of course, all this sequencing of statements is useless unless the rlm@2: statements have side effects. An important side effect is assignment. For rlm@2: example, one often uses assignment to place intermediate results in a named rlm@2: location (i.e. a variable) so that they may be used more than once later rlm@2: without recomputing them: rlm@2:
rlm@2: rlm@2: rlm@2: rlm@2:begin rlm@2: real a2, sqrtdisc; rlm@2: a2 := 2*a; rlm@2: sqrtdisc := sqrt(b^2 - 4*a*c); rlm@2: root1 := (- b + sqrtdisc) / a2; rlm@2: root2 := (- b - sqrtdisc) / a2; rlm@2: print(root1); rlm@2: print(root2); rlm@2: print(root1 + root2); rlm@2: end rlm@2: rlm@2:rlm@2: rlm@2: rlm@2: rlm@2: rlm@2:
rlm@2: It is well known that such naming of intermediate results may be accomplished rlm@2: by calling a function and binding the formal parameter variables to the rlm@2: results: rlm@2:
rlm@2: rlm@2: rlm@2: rlm@2:(fn [a2 sqrtdisc] rlm@2: ((fn[root1 root2] rlm@2: (do (println root1) rlm@2: (println root2) rlm@2: (println (+ root1 root2)))) rlm@2: (/ (+ (- b) sqrtdisc) a2) rlm@2: (/ (- (- b) sqrtdisc) a2)) rlm@2: rlm@2: (* 2 a) rlm@2: (sqrt (- (* b b) (* 4 a c)))) rlm@2: rlm@2:rlm@2: rlm@2: rlm@2: rlm@2:
rlm@2: This technique can be extended to handle all simple variable assignments which rlm@2: appear as statements in blocks, even if arbitrary G0 T0 statements also appear rlm@2: in such blocks. {Note Mccarthywins} rlm@2:
rlm@2:rlm@2: For example, here is a program which uses G0 TO statements in the form rlm@2: presented before; it determines the parity of a non-negative integer by rlm@2: counting it down until it reaches zero. rlm@2:
rlm@2: rlm@2: rlm@2: rlm@2:begin rlm@2: L1: if a = 0 then begin parity := 0; go to L2; end; rlm@2: a := a - 1; rlm@2: if a = 0 then begin parity := 1; go to L2; end; rlm@2: a := a - 1; rlm@2: go to L1; rlm@2: L2: print(parity); rlm@2: rlm@2:rlm@2: rlm@2: rlm@2: rlm@2: rlm@2:
rlm@2: This can be expressed in SCHEME: rlm@2:
rlm@2: rlm@2: rlm@2: rlm@2:(let rlm@2: [L1 (fn [a parity] rlm@2: (if (zero? a) (L2 a 0) rlm@2: (L3 (dec a) parity))) rlm@2: L3 (fn [a parity] rlm@2: (if (zero? a) (L2 a 1) rlm@2: (L1 (dec a) parity))) rlm@2: L2 (fn [a parity] rlm@2: (println parity))] rlm@2: (L1 a parity)) rlm@2: rlm@2:rlm@2: rlm@2: rlm@2: rlm@2: rlm@2:
rlm@2: The trick is to pass the set of variables which may be altered as arguments to rlm@2: the label functions. {Note Flowgraph} It may be necessary to introduce new rlm@2: labels (such as L3) so that an assignment may be transformed into the binding rlm@2: for a function call. At worst, one may need as many labels as there are rlm@2: statements (not counting the eliminated assignment and GO TO statements). rlm@2:
rlm@2:3.4 Compound Expressions '
rlm@2:At this point we are almost in a position to model the most general rlm@2: form of compound statement. In LISP, this is called the 'PROG feature". In rlm@2: addition to statement sequencing and G0 T0 statements, one can return a value rlm@2: from a PROG by using the RETURN statement. rlm@2:
rlm@2:
rlm@2: Let us first consider the simplest compound statement, which in SCHEME
rlm@2: we call BLOCK. Recall that
rlm@2: (BLOCK s1 sz)
is defined to be ((lambda (dummy) s2) s1)
rlm@2:
rlm@2: Notice that this not only performs Sl before S2, but also returns the value of
rlm@2: S2. Furthermore, we defined (BLOCK S1 S2 ... Sn)
so that it returns the value
rlm@2: of Sn
. Thus BLOCK may be used as a compound expression, and models a LISP
rlm@2: PROGN, which is a PROG with no G0 T0 statements and an implicit RETURN of the
rlm@2: last ``statement'' (really an expression).
rlm@2:
rlm@2: Host LISP compilers compile D0 expressions by macro-expansion. We have rlm@2: already seen one way to do this in SCHEME using only variable binding. A more rlm@2: common technique is to expand the D0 into a PROG, using variable assignments rlm@2: instead of bindings. Thus the iterative factorial program: rlm@2:
rlm@2: rlm@2: rlm@2: rlm@2:(oarxnz FACT . rlm@2: (LAMaoA (n) . rlm@2: (D0 ((M N (- H 1)) rlm@2: (Ans 1 (* M Ans))) rlm@2: ((- H 0) A"$)))) rlm@2: rlm@2:rlm@2: rlm@2: rlm@2: rlm@2: rlm@2:
rlm@2: would expand into: rlm@2:
rlm@2: rlm@2: rlm@2: rlm@2:(DEFINE FACT rlm@2: . (LAMeoA (M) rlm@2: (PRO6 (M Ans) rlm@2: (sszro M n rlm@2: Ans 1) ~ rlm@2: LP (tr (- M 0) (RETURN Ans)) rlm@2: (ssero M (- n 1) rlm@2: Ans (' M Ans)) rlm@2: (60 LP)))) rlm@2: rlm@2:rlm@2: rlm@2: rlm@2: rlm@2: rlm@2:
rlm@2: where SSETQ is a simultaneous multiple assignment operator. (SSETQ is not a rlm@2: SCHEME (or LISP) primitive. It can be defined in terms of a single assignment rlm@2: operator, but we are more interested here in RETURN than in simultaneous rlm@2: assignment. The SSETQ's will all be removed anyway and modelled by lambda rlm@2: binding.) We can apply the same technique we used before to eliminate G0 T0 rlm@2: statements and assignments from compound statements: rlm@2:
rlm@2: rlm@2: rlm@2: rlm@2:(DEFINE FACT rlm@2: (LAHBOA (I) rlm@2: (LABELS ((L1 (LAManA (M Ans) rlm@2: (LP n 1))) rlm@2: (LP (LAMaoA (M Ans) rlm@2: (IF (- M o) (nztunn Ans) rlm@2: (£2 H An$)))) rlm@2: (L2 (LAMaoA (M An ) rlm@2: (LP (- " 1) (' H flN$))))) rlm@2: (L1 NIL NlL)))) rlm@2: rlm@2:rlm@2: rlm@2: rlm@2:
rlm@2: clojure rlm@2:
rlm@2:rlm@2: We still haven't done anything about RETURN. Let's see… rlm@2:
-
rlm@2:
- ==> the value of (FACT 0) is the value of (Ll NIL NIL) rlm@2: rlm@2:
- ==> which is the value of (LP 0 1) rlm@2: rlm@2:
- ==> which is the value of (IF (= 0 0) (RETURN 1) (L2 0 1)) rlm@2: rlm@2:
- ==> which is the value of (RETURN 1) rlm@2: rlm@2:
rlm@2: Notice that if RETURN were the identity function (LAMBDA (X) X), we would get the right answer. This is in fact a rlm@2: general truth: if we Just replace a call to RETURN with its argument, then rlm@2: our old transformation on compound statements extends to general compound rlm@2: expressions, i.e. PROG. rlm@2:
rlm@2:4 Continuations
rlm@2:Up to now we have thought of SCHEME's LAMBDA expressions as functions,
rlm@2: and of a combination such as (G (F X Y))
as meaning ``apply the function F to
rlm@2: the values of X and Y, and return a value so that the function G can be
rlm@2: applied and return a value …'' But notice that we have seldom used LAMBDA
rlm@2: expressions as functions. Rather, we have used them as control structures and
rlm@2: environment modifiers. For example, consider the expression:
rlm@2: (BLOCK (PRINT 3) (PRINT 4))
rlm@2:
rlm@2: This is defined to be an abbreviation for:
rlm@2: ((LAMBDA (DUMMY) (PRINT 4)) (PRINT 3))
rlm@2:
rlm@2: We do not care about the value of this BLOCK expression; it follows that we
rlm@2: do not care about the value of the (LAMBDA (DUMMY) ...)
. We are not using
rlm@2: LAMBDA as a function at all.
rlm@2:
rlm@2: It is possible to write useful programs in terms of LAHBDA expressions rlm@2: in which we never care about the value of any lambda expression. We have rlm@2: already demonstrated how one could represent any "FORTRAN" program in these rlm@2: terms: all one needs is PROG (with G0 and SETQ), and PRINT to get the answers rlm@2: out. The ultimate generalization of this imperative programing style is rlm@2: continuation-passing. (Note Churchwins} . rlm@2:
rlm@2: rlm@2:4.1 Continuation-Passing Recursion
rlm@2:Consider the following alternative definition of FACT. It has an extra rlm@2: argument, the continuation, which is a function to call with the answer, when rlm@2: we have it, rather than return a value rlm@2:
rlm@2: rlm@2: rlm@2: rlm@2:procedure Inc!(n, c); value n, c; rlm@2: integer n: procedure c(integer value); rlm@2: if n-=0 then c(l) else rlm@2: begin rlm@2: procedure l(!mp(d) value a: integer a; rlm@2: c(mm); rlm@2: Iacl(n-1, romp): rlm@2: end; rlm@2: rlm@2:rlm@2: rlm@2: rlm@2: rlm@2: rlm@2: rlm@2:
(in-ns 'categorical.imperative) rlm@2: (defn fact-cps[n k] rlm@2: (if (zero? n) (k 1) rlm@2: (recur (dec n) (fn[a] (k (* n a)))))) rlm@2: rlm@2:rlm@2: rlm@2: rlm@2:
rlm@2: clojure rlm@2: It is fairly clumsy to use this version of‘ FACT in ALGOL; it is necessary to rlm@2: do something like this: rlm@2:
rlm@2: rlm@2: rlm@2: rlm@2:begin
rlm@2: integer ann
rlm@2: procedure :emp(x); value 2:; integer x;
rlm@2: ans :1 x;
rlm@2: ]'act(3. temp);
rlm@2: comment Now the variable "am" has 6;
rlm@2: end;
rlm@2:
rlm@2:
rlm@2:
rlm@2:
rlm@2:
rlm@2:
rlm@2:
rlm@2: Procedure fact
does not return a value, nor does temp
; we must use a side
rlm@2: effect to get the answer out.
rlm@2:
rlm@2: FACT
is somewhat easier to use in SCHEME. We can call it like an
rlm@2: ordinary function in SCHEME if we supply an identity function as the second
rlm@2: argument. For example, (FACT 3 (LAMBDA (X) X))
returns 6. Alternatively, we
rlm@2: could write (FACT 3 (LAHBDA (X) (PRINT X)))
; we do not care about the value
rlm@2: of this, but about what gets printed. A third way to use the value is to
rlm@2: write
rlm@2: (FACT 3 (LAMBDA (x) (SQRT x)))
rlm@2: instead of
rlm@2: (SQRT (FACT 3 (LAMBDA (x) x)))
rlm@2:
rlm@2: In either of these cases we care about the value of the continuation given to rlm@2: FACT. Thus we care about the value of FACT if and only if we care about the rlm@2: value of its continuation! rlm@2:
rlm@2:
rlm@2: We can redefine other functions to take continuations in the same way.
rlm@2: For example, suppose we had arithmetic primitives which took continuations; to
rlm@2: prevent confusion, call the version of "+" which takes a continuation '++",
rlm@2: etc. Instead of writing
rlm@2: (- (+ B Z)(* 4 A C))
rlm@2: we can write
rlm@2:
(in-ns 'categorical.imperative) rlm@2: (defn enable-continuation "Makes f take a continuation as an additional argument."[f] rlm@2: (fn[& args] ((fn[k] (k (apply f (reverse (rest (reverse args)))))) (last args)) )) rlm@2: (def +& (enable-continuation +)) rlm@2: (def *& (enable-continuation *)) rlm@2: (def -& (enable-continuation -)) rlm@2: rlm@2: rlm@2: (defn quadratic[a b c k] rlm@2: (*& b b rlm@2: (fn[x] (*& 4 a c rlm@2: (fn[y] rlm@2: (-& x y k)))))) rlm@2: rlm@2:rlm@2: rlm@2: rlm@2: rlm@2: rlm@2:
rlm@2: where k
is the continuation for the entire expression.
rlm@2:
rlm@2: This is an obscure way to write an algebraic expression, and we would rlm@2: not advise writing code this way in general, but continuation-passing brings rlm@2: out certain important features of the computation: rlm@2:
-
rlm@2:
- 1 The operations to be performed appear in the order in which they are rlm@2: rlm@2:
performed. In fact, they must be performed in this rlm@2: order. Continuation- passing removes the need for the rule about rlm@2: left-to-right argument evaluation{Note Evalorder) rlm@2:
-
rlm@2:
- 2 In the usual applicative expression there are two implicit
rlm@2: temporary values: those of
(* B B)
and(* 4 A C)
. The first of rlm@2: these values must be preserved over the computation of the second, rlm@2: whereas the second is used as soon as it is produced. These facts rlm@2: are manifest in the appearance and use of the variable X and Y in rlm@2: the continuation-passing version. rlm@2:
rlm@2:
rlm@2: In short, the continuation-passing version specifies exactly and rlm@2: explicitly what steps are necessary to compute the value of the rlm@2: expression. One can think of conventional functional application rlm@2: for value as being an abbreviation for the more explicit rlm@2: continuation-passing style. Alternatively, one can think of the rlm@2: interpreter as supplying to each function an implicit default rlm@2: continuation of one argument. This continuation will receive the rlm@2: value of the function as its argument, and then carry on the rlm@2: computation. In an interpreter this implicit continuation is rlm@2: represented by the control stack mechanism for function returns. rlm@2: Now consider what computational steps are implied by: rlm@2:
rlm@2:
rlm@2: (LAMBDA (A B C ...) (F X Y Z ...))
when we "apply" the LAMBDA
rlm@2: expression we have some values to apply it to; we let the names A,
rlm@2: B, C … refer to these values. We then determine the values of X,
rlm@2: Y, Z … and pass these values (along with "the buck",
rlm@2: i.e. control!) to the lambda expression F (F is either a lambda
rlm@2: expression or a name for one). Passing control to F is an
rlm@2: unconditional transfer. (Note Jrsthack) {Note Hewitthack) Note that
rlm@2: we want values from X, Y, Z, … If these are simple expressions,
rlm@2: such as variables, constants, or LAMBDA expressions, the evaluation
rlm@2: process is trivial, in that no temporary storage is required. In
rlm@2: pure continuation-passing style, all evaluations are trivial: no
rlm@2: combination is nested within another, and therefore no ‘hidden
rlm@2: temporaries" are required. But if X is a combination, say (G P Q),
rlm@2: then we want to think of G as a function, because we want a value
rlm@2: from it, and we will need an implicit temporary place to keep the
rlm@2: result while evaluating Y and Z. (An interpreter usually keeps these
rlm@2: temporary places in the control stack!) On the other hand, we do not
rlm@2: necessarily need a value from F. This is what we mean by tail-
rlm@2: recursion: F is called tail-recursively, whereas G is not. A better
rlm@2: name for tail-recursion would be "tail-transfer", since no real
rlm@2: recursion is implied. This is why we have made such a fuss about
rlm@2: tail-recursion: it can be used for transfer of control without
rlm@2: making any commitment about whether the expression expected to
rlm@2: return a value. Thus it can be used to model statement-like control
rlm@2: structures. Put another way, tail—recursion does not require a
rlm@2: control stack as nested recursion does. In our models of iteration
rlm@2: and imperative style all the LAMBDA expressions used for control (to
rlm@2: simulate GO statements, for example) are called in tail-recursive
rlm@2: fashion.
rlm@2:
4.2 Escape Expressions
rlm@2:Reynolds [Reynolds 72] defines the construction rlm@2: = escape x in r = rlm@2:
rlm@2:rlm@2: to evaluate the expression \(r\) in an environment such that the variable \(x\) is rlm@2: bound to an escape function. If the escape function is never applied, then rlm@2: the value of the escape expression is the value of \(r\). If the escape function rlm@2: is applied to an argument \(a\), however, then evaluation of \(r\) is aborted and the rlm@2: escape expression returns \(a\). {Note J-operator} (Reynolds points out that rlm@2: this definition is not quite accurate, since the escape function may be called rlm@2: even after the escape expression has returned a value; if this happens, it rlm@2: “returns again"!) rlm@2:
rlm@2:rlm@2: As an example of the use of an escape expression, consider this rlm@2: procedure to compute the harmonic mean of an array of numbers. If any of the rlm@2: numbers is zero, we want the answer to be zero. We have a function hannaunl rlm@2: which will sum the reciprocals of numbers in an array, or call an escape rlm@2: function with zero if any of the numbers is zero. (The implementation shown rlm@2: here is awkward because ALGOL requires that a function return its value by rlm@2: assignment.) rlm@2:
rlm@2: rlm@2: rlm@2:begin rlm@2: real procedure Imrmsum(a, n. escfun)§ p rlm@2: real array at integer n; real procedure esciun(real); rlm@2: begin rlm@2: real sum; rlm@2: sum := 0; rlm@2: for i :1 0 until n-l do rlm@2: begin rlm@2: if a[i]=0 then cscfun(0); rlm@2: sum :1 sum ¢ I/a[i]; rlm@2: enm rlm@2: harmsum SI sum; rlm@2: end: . rlm@2: real array b[0:99]: rlm@2: print(escape x in I00/hm-m.mm(b, 100, x)); rlm@2: end rlm@2: rlm@2:rlm@2: rlm@2: rlm@2: rlm@2: rlm@2:
rlm@2: If harmsum exits normally, the number of elements is divided by the sum and
rlm@2: printed. Otherwise, zero is returned from the escape expression and printed
rlm@2: without the division ever occurring.
rlm@2: This program can be written in SCHEME using the built-in escape
rlm@2: operator CATCH
:
rlm@2:
abc rlm@2: rlm@2:rlm@2: rlm@2: rlm@2: rlm@2: rlm@2:
Date: 2011-07-15 02:49:04 EDT
rlm@2: rlm@2:Org version 7.6 with Emacs version 23
rlm@2: Validate XHTML 1.0 rlm@2: