Comput. Lang. Vol. 17, No. 1, pp. 19-37, 1992 Printed in Great Britain. All rights reserved
0096-0551/92 $3.00 + 0.00 Copyright © 1991 Pergamon Press plc
A CONTINUATION-BASED LANGUAGE EMBEDDED IN SCHEME CHING-LIN WANG Department of Computer Science, T a m k a n g University, Tamsui, Taipei 25137, Taiwan, Republic of China
(Received 4 January 1990; revision received 25 September 1990) A b s t r a c t - - W e show that lambda abstractions can be transformed into continuations with several interesting characteristics: • • • •
Continuations could serve as the basis of a Scheme-like language, called Scheme-2p The transformation is analogous to the continuation-passing style transformation. Several programming-language concepts emerge as results of the transformation. Everything that can be represented as a lambda abstraction can now be represented as a continuation. • The transformation is simple and sound. Our work would not be complete if the macro facility were not taken into account. Thus, we show how to embed Scheme-2 t in Scheme so as to accommodate the macro facility.
Continuations
Embeddings Programming-languageconcepts
Scheme
1. I N T R O D U C T I O N In a previous paper [1], we showed that first-class continuations in Scheme [2, 3] can be used to implement lazy evaluation. In essence, our work there was to represent nullary lambda abstractions as continuations, and to replace applications of nullary lambda abstractions by applications of the corresponding continuations. In this paper, we go further to an extreme to show that every lambda abstraction, of any arity, can be represented as and systematically transformed into a continuation. Transforming lambda abstractions into continuations is interesting for several reasons: • Lambda need not be treated as a core syntactic form. It could be superseded by one that acts essentially as a hybrid of call/cc and let for creating local bindings of continuations as in call/cc and any objects as in let, or by an even simpler one that allows creating local bindings of continuations as in call/cc. We refer to the former as let/cc and the latter as block/cc. Substituting them for lambda as core syntactic forms and removing call/cc result in two continuation-based languages, say Scheme-;q and Scheme-~.2 respectively, which are equivalent to Scheme. • As far as "continuation-passing" is concerned, the transformation is closely related to the well-known continuation-passing style transformation [4]. But the transformation has distinct objective and characteristics. • There are a number of possible transformations; and the transformed code could be either functional or imperative. With these transformations we may talk of a number of programming-language concepts which are usually sensible only to assembly-language programmers. Among them are the concepts of return addresses, frames, run-time stack, anonymous formal parameters, prologue and epilogue instructions for functions, "assignment is just mutation", static and dynamic chains, and such. In a sense, we are using continuations to simulate and elucidate the low-level control structures underlying the implementations of block-structured languages. This is in contrast with the common use of continuations to obtain advanced control structures such as, for example, coroutines [5, 6], break facilities [6] and nonblind backtracking [7, 8]. Thus, to say the least of it we have created a new dimension of use and enhanced our understanding of continuations. 19
20
CFIING-LIN WANG
• Everything that can be represented as a lambda abstraction can now be represented as a continuation. In particular, most of the data objects (e.g. natural numbers, truth values [9]) and data structures (e.g. streams, lazy data structures [10]) as well as the associated operators needed for modem programming can, at least theoretically, be represented as continuations
[1, 111. • Although the transformed code, expressed entirely with continuations, will be unpleasantly lengthy and hard to read even for just a few lines of source code, the transformation rules are actually quite simple and sound. As a matter of fact, this deficiency could be removed by outfitting the target languages with a few syntactic extensions. For reasons that will become clear later, we shall in this paper focus only on the language Scheme-2~. The language Scheme-2~ is essentially a subset of Scheme in which lambda is constrained to be used as a block constructor. Being a subset, Scheme-2~ cannot be more expressive than Scheme. On the other hand, if every (core) Scheme expression can be transformed into an equivalent Scheme-21 expresion then Scheme-21 will be no less expressive than Scheme, and in fact the two languages will be equivalent. One main part of the paper is therefore dedicated to the issue of equivalence. That is, we present a syntactic transformation function that maps core Scheme expressions into equivalent Scheme-2~ expressions. At this point, we must emphasize that the transformation also serves to show that continuations in Scheme could be manipulated in such picturesque ways to make the aforementioned programming-language concepts vivid--which is one of the main purposes of the paper. In addition to using Scheme-21 as the target language of the transformation, we also plan to realize it. We do this by embedding Scheme-2~ in Scheme. Following from [12], the embedding is done by realizing the core syntactic form let/cc of Scheme-2~ as a syntactic extension in Scheme. Embeddings of this sort avoid the need for an interpreter for the embedded language Scheme-21. So far as the core language is concerned, realizing let/cc of Scheme-21 as a syntactic extension in Scheme is sufficient for the embedding. But if the macro facility is also taken into account, the syntactic extension alone will be incompetent. Put differently, towards the supreme end of representing every lambda abstraction as a continuation, our work would not be complete if the macro facility were not taken into consideration. Towards that end, the full embedding requires Scheme-21 to be equipped with its own macro processor. The macro processor, however, can be easily obtained. In the next section we briefly review a Scheme subset sufficient for the purposes of this paper. Section 3 takes a simple example to demonstrate the ideas of our transformation. The relationship to the well-known continuation-passing style transformation is briefly discussed. A new view unique to our transformation is mentioned. The core syntactic forms let/cc and block/cc as well as the resulting languages Scheme-2~ and Scheme-22 are defined. The core syntactic form let/cc is also realized as a syntactic extension in Scheme, thus forming a partial embedding. Section 4 presents the syntactic transformation function that transforms core Scheme expressions, excluding the type predicates for functions, into Scheme-2~ expressions. A couple of variations of transforming lambda abstractions are also given. Taking the type predicates for functions into consideration, Section 5 then justifies the equivalence of the two languages. Section 6 presents several useful macros for Scheme-2~. Section 7 completes the embedding of Scheme-2~ in Scheme. The final section presents a conclusion. 2. SCHEME For the purposes of this paper by core Scheme we mean the language with the following constructs: (expression) :: = (constant) (identifier) (quote (expression)) (define (identifier) (expression)) (set ! (identifier) (expression)) (if (expression) (expression) (expression)) (lambda ({(identifier)}) (expression) {(expression)}) (application) (application) :: = ((expression) {(expression)})
A continuation-based language embedded in Scheme
21
We assume for simplicity that define is a core syntactic form and can only appear as top-level definitions.t The core language also contains a number of primitive functions, including especially call/cc and several type predicates for functions. For detailed descriptions of call/cc we refer the reader to [6, 14, 15]. Suffice it to say here that call/cc must be passed a unary function, and any unary (primitive, lambda, or even continuation) function will do. The current standard of Scheme treats all functions as a single type, with procedure? as a type predicate [2]. For the purposes of this paper, we regard primitive and lambda functions as a type (with closure? as a type predicate), which is distinct from the type of continuation functions (with continuation? as a type predicate) [16]. Among these three type predicates, any one could be expressed using the remaining two, and so only two of them need to be primitive. For the sake of simplicity, however, we treat all of them as primitives. The transformation will take care of only the core language. For the purposes of the embedding, we need a macro facility for defining syntactic extensions. As yet, the macro facility has not been standardized [2]; but many implementations of Scheme do provide their own. The most convenient facilities are macro [16, 17] and extend-syntax [14]. We assume the existence of extend-syntax. (With the extend-syntax facility at hand, one may obtain the " m a c r o " facility. See [6, p. 455].) For the purposes of this paper, we regard extend-syntax per se as a syntactic extension defined using a binary primitive function install-macro. Thus, for example, the macro call (extend-syntax (let) [(let (Iv e ] . . . ) expl e x p 2 . . . ) ((lambda ( v . . . ) expl e x p 2 . . . ) e . . . ) ] ) will be expanded by the macro processor expand into something like (install-macro 'let (lambda (x) '((lambda,(map car (cadr x)) ,@(cddr x)) ,@(map cadr (cadr x))))) which is then evaluated by the standard evaluation function eval. The effect is to bind the keyword let, in some manner, to the corresponding expansion function (the second argument of installmacro). Similar to call/cc, any unary function could serve as an expansion function. Two unary primitive functions macro-function and macro?, both are passed an argument which must evaluate to an identifier, are also assumed to exist, macro-function returns the expansion function, if any, bound to that identifier; otherwise the value returned is unspecified, macro? checks if that identifier is a keyword of some syntactic extension. We also make use of quasiquote or backquote('), unquote(,), unquote-splicing (,@) [2], and the function gensym which generates an uninterned symbol that is guaranteed to be distinct from all other symbols. 3. BASIC I D E A S
3.1. The relationship to continuation-passing style transformation The basic idea of our transformation is analogous to the well-known continuation-passing style (henceforth, cps) transformation: each lambda function is added an extra parameter--a continua t i o n - t o which the lambda function supplies its result. The main differences between our transformation and the cps transformation are twofold: • In cps transformation, the added continuation is expressed and constructed explicitly as a lambda function, whereas in ours it is obtained implicitly by means of the primitive function call/cc. • The cps transformation is primarily used for removing recursions [18], whereas, towards the goal of eliminating the need for the core syntactic form lambda, ours plans to replace every lambda function, recursive or not, by a continuation, taking no account of recursionremoval. tDefine needs not be treated as a core syntactic form [13]. Internal definitionscan always be converted into an equivalent letrec expression, which in turn can be converted into an equivalent lambda application [2, 10].
22
CmNG-LIN WANG (define fib (lambda (n ret) (if (<= n I) (let n) (fib (--n I)
(define fib (lambch (n) (if (
0xmbdx(-I)
(+ (fib (- n 1)) (fib (- n 2))))))
(fib (- n 2) (lambd& (v2) (le~ (+ vl v2)))))))))
Fig. l.(a) Lambda function fib.
Fig. l.(b) Cps function fib.
(define fib
(call/cc Oxmb~ (c) (let ([frame (call/cc (lambda (fib-entry) (c fib-entry)))]) (let ([let (list-~ef ~ame 0)] In (list~ef frame I)]) (let (if (<-----n I) n
(+ (ca~/cc (lambda (retl) (fib (list retl ( - n 1)))))
(c~Icc (lambd~(let2) (fib (llst rvt2 (- n 2)))))))))))))) Fig. 1.(c) Continuation function fib.
Let us consider the simple example, that of the fibonacci function, shown in Fig. 1. Figure l(a) shows the lambda function fib, Fig. l(b) the corresponding cps function, and Fig. l(c) the corresponding continuation function. Notice that in Fig. l(c) we still take advantage of the core syntactic form lambda. We shall turn back to this issue in Subsection 3.3. Notice also that the transformed code is pure functional. On evaluating the define expression of Fig. 1(c), the variable c will be bound to a continuation which represents essentially the rest of the computation from the point of evaluating the lambda expression of Fig. l(a) and may be described by (lambda (v) (define fib v)) The application (c fib-entry) then binds the variable fib to the value of fib-entry, which is a continuation representing essentially the entry point of the function fib and may be described as (lambda (v) (let ([frame v]) --the inner let expression--)) A call of the original lambda function fib, say (fib n), will now be expressed as (call/cc (lambda (ret) (fib (list ret n)))) This causes the variable frame to be bound to a list formed by the call-time continuation ret and the parameter n. Using the same calling conventions for the two recursive calls, the desired nth fibonacci number is then computed and returned to the caller by invoking the call-time continuation. We now turn to the relationship to the cps transformation. First of all, if the preceding call is entered at the Scheme top level then the call-time continuation bound to ret may be described as (lambda (v) v)
A continuation-basedlanguage embedded in Scheme
23
coinciding with the initial continuation used in the cps transformation. In the course of the computation, the variable retl--the argument added to the call (fib (--n l))--will be bound to a continuation described by (lambda (vl) (ret ( + vl (call/cc (lambda (ret2) (fib (list ret2 ( - n 2)))))))) and ret2--the argument added to the call (fib ( - n 2))---wiU be bound to (lambda (v2) (ret( + vl v2))) (Here we tacitly assumed, without loss of generality, that the two recursive calls are performed in the stated order.t) Clearly, the continuation ret2 is identical to the corresponding one used in the cps function. On the other hand, it might seem at first glance that the continuation retl is different from the corresponding one used in the cps function. A moment's thought, however, reveals the reverse: retl essentially means that "First takes the value vl, then invokes (fib ( - n 2)), passing also the continuation ret2 (and that is all!)"; thus, although expressed distinctly, it is in fact analogous to the corresponding continuation in the cps function. So far, we have demonstrated that as far as "continuation-passing" is concerned our transformation is analogous to the cps transformation. However, the objective of our transformation, as stated earlier, is quite different from that of the cps transformation. To give a case in point, notice that it makes no sense to transform the cps function of Fig. l(b) using the cps transformation, whereas our transformation is applicable as the cps function per se involves lambda functions and applications. 3.2. A new view to our transformation
We now shift our point of view slightly to make mention of another feature unique to our transformation: viewing the call-time continuation as the return address, we may deem the list passed to the continuation function fib as a frame, i.e. an activation record. As Fig. 1(c) shows, each time the continuation function fib is invoked two copies of the frame will be made due to the two related let expressions. There are, however, a couple of variations that can be used to remedy this deficiency. Moreover, with frames at our disposal we may talk of a number of programming-language concepts with appropriate transformations. All these will be discussed later in Subsection 4.2. 3.3. The language Scheme-2~
Looking back to Fig. l(c), we see that after transforming the lambda function fib into a continuation function, there are still two places where lambda appears: one in call/cc and the other (implicitly) in let. Observe that it makes no sense to transform the lambda functions and applications embedded in these two cases into continuation functions and applications as otherwise we will keep busy doing so. Thus, it seems that we are unable to substitute continuation function and application for every occurrence of lambda function and application. Or, to put it in another way, lambda as a core syntactic form is still necessary. Nevertheless, our justification for removing lambda is as follows. Lambda, used only in the context of call/cc and let, has lost its full power--it is no longer a first-class object, and in fact degenerates into a block constructor. Therefore, to avoid using great talent to deal with small matters, lambda does not necessarily have to be treated as a core syntactic form. All that is needed is one that acts essentially as a hybrid of call/cc and let, permitting the creation of local bindings of continuations as in call/cc and any objects as in let. tThe evaluation order of operator and operand(s) in a function application is unspecifiedin Scheme [2].
24
CHING-LINWANG (extezd--~yntax (let/cc)
[(let/cc (Iv el...) expl exp2 ...) (with ([([vl eli ...) (filter (lambda (p) (not (eq? (cadr p) ':cc:))) '([v e] ...))] [(,,2 ...) (map c~
(filter (lsmbda (p) (eq? (cadr p) ':cc:)) ,([v e] ...)))]) (lambda--gea ([vl el] ...) (v2 ...) expl exp2 ...))]) (extend-syntax (lambda--gen)
[(lambda--gen(Ivel ...)0 expl exp2 ...) ((lambda (v ...)expl exp2 ...)e ...)] [(lambda--ge~0 (v ...)expl exp2 ...) (c~Ulcc--gen(v ...)expl exp2...)] [(lambda-gen ([vlell ...)(v2 ...)expl exp2 ...) ((lambda (vl ...) (call / cc--~en (v2 ...) exp l exp2 ...))
el ...)]) (extend--syntax (call/cc-gen) [(call/cc--gen(v) expl exp2 ...) (c~U/cc(lmbda (v) expl exp2...))] [(c~I/cc---gen(vl v2 ...)expl exp2 ...) (c~ll/cc (lambda (vl) (c~/cc-se~ (v2 ...) e~pZ e~p2 ...)))]) Fig. 2. Let/co as a macro in Scheme.
To this end, let us define a language, say Scheme-2z (read as "Scheme minus 4, version 1"), as (core) Scheme minus lambda and call/cc, plus let/cc. Here, let/ee is the core syntactic form to fit the need as stated above. The syntax of let/cc is analogous to that of let
(let/~ (Iv, e l l . . . [Vrn em]) expl . . . exPn) except that each ei is allowed to be either an expression in Scheme-2! or a special marker :c¢:t The intended meaning is straightforward: If ei is a Seheme-2~ expression then vi is bound to the value of ei; on the other hand, if ei is the marker :cc: then vi is bound to the continuation current at the time the let/co expression is evaluated. Except for this, let/cc obeys the same semantics, including especially the scoping rule and the arbitrary evaluation order of the eis, of let. The core syntactic form let/co of Scheme-21 can in itself be simulated as a syntactic extension in terms of lambda and call/cc in Scheme, as shown in Fig. 2. The realization of let/cc results in an embedding of Scheme-21 in Scheme. The embedding, however, is incomplete in case the macro facility is taken into account, as we shall see in Section 7. The functions filter and map in Fig. 2 have their usual meanings. A list of pairs ([Vl ell. •. [Vmem]) in let/cc is first split up into two sublists: the first groups together those pairs whose e~s are not the marker :cc:, and the second collects the remaining pairs with the markers dropped. The auxiliary macro lambda-gen then generates a lambda application as directed by the first sublist, and
t T h e marker :co: is not a variable, but may be thought as one bound to the continuation current at the time the immediately enclosing let/cc is evaluated. It is not really necessary as we can stipulate that missing an e~ is precisely the same as presenting a :cc: (All special names in Scheme-A, will begin with a colon. The marker also ends up with a colon to emphasize that it is not a variable.)
A continuation-based language embedded in Scheme
25
call/cc-gen generates a sequence of call/cc expressions as guided by the second sublist. A general macro call to let/cc of the form (let/cc ([v, ell .. [vi ei]
[k~ :cc:]... [kj :cc:]) expl . . . eXpn) where i, j, n >/1, will, taking into account only let/cc, be expanded intot ((lambda (Vl... vi) (call/cc (lambda (kl) (call/cc (lambda (kj) e x p l . . , exp.))...))) e~... ei) It is not hard to see, by ~/-conversion, that the variables kmS are all bound to the continuation current at the time the (outermost) lambda abstraction is applied, as desired. It should be noted that the sequence of call/ccs cannot be pulled outwards to enclose the lambda application for otherwise the stipulated scoping requirement will be violated, though the variable kms will still be correctly bound. It is worth while mentioning that the preceding let/cc expression can, while preserving its semantics, be split up into two let/ccs in the following order: (let/cc ([v, e l l . . . [vi ei]) ( l e t / c c ([k I : c c : ] . . .
[kj :cc:])
e x p l . . , expn)) In other words, it is never necessary to make use of let/cc in its mixed form: it could be used either solely as a substitute for call/cc or solely as a substitute for let. Of course, we could have restricted the use of let/cc or defined two distinct core syntactic forms at the outset, but neither appears to outweigh the neat syntax and semantics of let/cc thus far described. From the realization of let/cc we see that Scheme-Al is essentially a subset of Scheme in which lambda is constrained to be used as a block constructor. A natural question now arises: Is Scheme-A~ equivalent to Scheme? One way to answer this is to see if every (core) Scheme expression can be transformed into an equivalent Scheme-At expression, and is the subject of the next two sections.
3.4. The language Scheme-A2 As stated, let/cc is a hybrid of call/cc and let used to supersede lambda as a core syntactic form. However, it is not the simplest one to the purpose. To see this, observe that in case for all i and j v~ does not occur free in ej and the storage for the vis will be allocated once and only once, the let expression (let ([v, e,]... [Vm era]) expt.. •exp.) f l f j = 0 then there will be no call/cc expressions. If i = 0 and j t> 1 then the outer lambda application will not be generated. These are ensured by the order of the [pattern expansion] pairs used to define lambda-gen.
26
CI-IING-LIN WANG
can be simulated by (call/cc (lambda (vl) ",.
(call/cc (lambda (Vm) (set! Vl e l ) . . . (set! Vm em)~" expl • • • expn))... )) Observe that among the two let expressions in the transformed code of Fig. l(c), the inner one satisfies the required conditions for the preceding simulation, but the outer one does not, because each time the continuation function fib is invoked (recursively) a new piece of storage will be allocated for the variable frame. Nevertheless, we can still replace both let expressions by the preceding simulating code without affecting the correctness of the whole transformation. What is affected is that each time the continuation function fib is invoked only one copy of the frame will be made (by the simulating code of the inner let expression). It follows from this observation that the simplest core syntactic form that can be used as a substitute for lambda is a call/cc-like syntactic form that allows creating local bindings of the continuation current at the time the form is evaluated. We are now in position to define the second version of our continuation-based languages, say Scheme-22, as (core) Scheme minus lambda and call/cc, plus block/cc. Here, block/cc-an alias of the syntactic extension call/cc-gen of Fig. 2--is the core syntactic form to fit the need as stated above. As an instance, the preceding simulating code will be expressed in Scheme-22 as (block/cc (vl . . . Vm) (set! v, e l ) . . . (set! Vm em) e x p l . . , expn) Obviously, Scheme-A2 is essentially a constrained subset of Scheme-2~ and an even more constrained subset of Scheme. To answer the question as to whether Scheme-22 is equivalent to Scheme we simply notice that the transformation of Scheme into Scheme-2~, to appear soon, can be easily modified to use Scheme-2: as the target language instead. This requires a simple proof, and is left to the reader. The primary drawbacks of Scheme-22 as a target language of our transformation are clearly twofold: the transformed code will necessarily be imperative, and much more importantly, cause the garbage collector to work terribly hard. For these reasons, we leave it out of consideration.
4. T R A N S F O R M I N G CORE S C H E M E INTO SCHEME-2t In this section we present the syntactic transformation function T mapping expressions, excluding the type predicates for functions (which we defer discussing until the next section), in core Scheme into equivalent expressions in Scheme-2j. Subsection 4.1 temporarily omits the primitive function call/cc. The transformation of call/cc will be discussed in Subsection 4.3. Subsection 4.2 gives a number of variations of transforming lambda abstractions.
4.1. The transformation In the absence of the primitive function call/cc, the transformation function T is defined as follows (where c denotes a constant, v with or without subscript an identifier, e with or without subscript a Scheme expression). fMore precisely, these m set! expressions should be randomly permuted in order to preserve the property that the ets are evaluated in an arbitrary order.
A continuation-based language embedded in Scheme
27
T~c~ --c T[v~ = v TE(quote e)~ = (quote e) T[[(define v e)~ = (define v TEe]]) TE(set! v e)~ - (set! v T[[e~) T[[(if e, e2 e3)~ = (if TEe~ T[e2~ T~e3~) TE(lambda ( v l . . . Vm) e l . . . e,)~ --(let/cc ([c :cc:]) (let/cc ([frame (let/cc ([entry :cc:]) (c entry))]) (let/cc ([ret (list-ref frame 0)] [v~ (list-ref frame 1)] [Vm (list-ref frame m)]) T[[el~... (ret TEen]])))) where the variables c, frame and ret do not occur free in ei, 1 ~ i ~ n, m >/0. T~(e, e2... e,)]] = (TEe, ~ TEe2~... T[e,~), if e, evaluates to a primitive function; = (let/cc ([ret :cc:]) (T[e~] (list ret TEe2~ ... TEen~))), if el evaluates to a lamhda function; where the variable ret does not occur free in ei, 1 ~< i ~< n. - ( l e t / c c ([fun T~el~][arg2TEe2~... [arg, T[en~]) (if (not (continuation? fun)) (fun arg2.., argo) (let/cc ([ret :cc:]) (fun (list ret arg2.., arg,))))), otherwise. Most of the transformation rules are straightforward: constants, identifiers and quote expressions are self-transformed as they involve no lambda abstractions and function applications; define, set! and if expressions are transformed by recursively transforming the subexpressions that may contain lambda abstractions and/or function applications. The remaining transformation rules deserve some comments. Lambda abstractions. By now, the general idea of transforming lambda abstractions should have been clear. Nevertheless, there are several points to speak of. First, the body of a lambda abstraction cannot be transformed as (ret (begin Tie , ~ . . . TEe~)) for begin is not a core syntactic form in Scheme-;., (and Scheme) as defined earlier, nor as (ret TE((lambda () TilerS... T[e,]]))~)f for this will make the function T circularly-defined. Second, we tacitly assumed that list-ref is a primitive function in Scheme-,~,. This is not strictly necessary as (list-ref frame i) could always be replaced by a sequence of i cdrs followed by a car. But notice that list-ref cannot be treated as a (recursive) lambda function and transformed accordingly, for otherwise the transformed continuation function will be nonterminating upon an invocation. These remarks also apply to the function list-tail to appear soon. Third, it is fairly clear that the variables c, frame and ret should not occur free in each ei and, consequently, T[[ei~. (With one minor exception: the variables c and frame, excluding ret, may occur free in any e i in case they happen to be formal parameters of the lambda abstraction being transformed.) Unless stated otherwise, for all of the transformations to be presented later we shall for convenience sake not state explicitly this kind of constraints as they can be easily inferred. 1"Begin treated as a macro could expand into this lambda application (in case none of the eis are definitions) [2].
28
CHING-LIN WANG
Function applications. To transform function applications we must distinguish two cases: primitive and lambda function applications, because in their transformed code these two cases have different calling conventions in the sense described below. (Remember that at present we do not yet consider call/cc, so no continuation function applications need considering at this point.) In either case the operator and operands of an application must be recursively transformed as they may contain lambda abstractions and/or function applications. In case the operator evaluates to a lambda function the transformed operands must be packaged with a return address to form a frame which is then passed to the transformed operator--a continuation function in Scheme-21. For primitive function applications, on the other hand, all that is required is to apply, as usual, the transformed operator--still primitive in Scheme-2~--directly to the transformed operands. This, however, is not the whole story. The problem here is that for a function application the expression to compute the function may, at different times, produce operators of different types.t This implies that in Scheme-2t some dynamic type checking--to discriminate between primitive and continuation functions--is necessary in case the type of the operator is unknown during the transformation process. For example, if the operator is a downward funarg then there is typically no way to tell its type in the course of the transformation. Therefore, to accomplish the transformation, we add a last case in which the predicate continuation? is used as a type checker. A few notes are in order. First, the predicate closure? could also act as a type checker. Second, in the last case the local variables arg~'s are not strictly necessary. They could very well be removed but then the transformed code of each ei would be reproduced twice. On the other hand, the local variable fun is indispensable or else the transformed code of the operator et would be evaluated twice upon an invocation, which is generally incorrect if e~ includes some side effects. Third, the transformation rule presented above is not well-defined in the sense that it does not specify how to determine the type of an operator. This is left to the implementation and is not the subject of this paper. (Keeping a symbol table may help.) Fourth, if we assume that all the primitive functions are of fixed and known arities then some slight changes to the preceding transformation function will make it well-defined as dynamic type checkings are no longer needed. The necessary changes are: T~[v~ = T~(lambda (v~... v,)(v v~... vn))~, if v is a n-ary primitive; = v, otherwise. T~(e, e2... en)~ = (e~ T[[e2~... T[e,~), if el is an identifier denoting a (n - l)-ary primitive; = (let/cc ([ret :cc:]) (T~ej~ (list ret T~e2]... T[Ve,~))), otherwise. The trick here is to convert every primitive function that does not appear as an operator into an equivalent, by q-conversion, lambda function. The effect of the conversion is to deprive primitive functions of their first-class citizenships. That is to say, they can neither be used as downward and upward funargs nor be stored in variables and data structures, and in fact can only appear as operators. It follows that for a function application the operator is either an identifier denoting a primitive function or a lambda function, and therefore no type checkings are required in the transformed code. Apparently, this latter approach does not work for all of the primitive functions as some of them are not of fixed arities. It is, however, meaningful to intermingle these two approaches--the former for those of varied arities and the latter for those of fixed arities. Moreover, as we shall see later, the latter approach will be used to transform call/cc as it is known to be a unary primitive. 4.2. Variations In this subsection we present a number of variations of transforming lambda abstractions. The first two work only for certain kinds of lambda abstractions. The classification of the variations ?Recall that we regard primitive (and lambda) functions and continuation functions as distinct types. The phrase "operators of different types" refers to the operator's type in this connection, rather than its domain and range types (as in the usual sense). Also, this statement has nothing to do with the weak typing of Scheme; it would remain true even if Scheme were strongly typed.
A continuation-based language embedded in Scheme
29
is by no means unique, but rather for better references. For example, the variation classified as Manipulating the static chain is also of Imperative style and Anonymous formal parameters. Nullary lambda abstractions. It is easily seen that nullary lambda abstractions and function applications could be transformed a little more concisely by standing alone the return address, i.e. without packaging it in a frame. We mention this special case here because it is essentially (not exactly) the transformation that we used in the previous paper. Generalized curriedfunctions. By generalized curried functions we mean lambda abstractions of the form similar to curried functions except that they may take any number of arguments, rather than one argument, at a time. That is, a generalized curried function is one that is made of at least two consecutive lambda abstractions. For such a function we notice that the call-time continuation of the lambda abstraction at one level is precisely the definition-time continuation of that at the immediately consecutive level. Thus, generalized curried functions could be transformed more succinctly by making proper use of the call-time continuations. We leave it to the reader. Imperative style. As mentioned earlier, the functional-style transformation of lambda abstractions will wastefully cause two copies of a frame to be made on each invocation of the transformed code. To remove the first copy, it suffices to replace the related let/cc expression by a set! expression, as shown below. T~[(lambda ( v l . . . Vm) e l . . . e,)~ = (let/cc ([c :cc:]) (set! c (let/cc ([entry :cc:]) (c entry))) (let/cc ([ret (list-ref c 0)] [Vl (list-ref c 1)] [Vm (list-ref C m)]) T~el ~ . . . (ret T~en~))) Observe that the variable c, bound initially to the definition-time continuation, will be rebound to the frame passed by a call of the continuation function. That the rebinding is safe is due to the fact that the definition-time continuation was used only once at the definition time. The primary drawback of this variation is that it is imperative, though only one copy of the frame is made. Anonymous formal parameters. On the other hand, the second copy can be easily removed if we are willing to use (list-ref frame i) for each access to the R-value, and (list-tail frame i) in combination with set-car! for each access to the L-value of the ith formal parameter. The transformation rule is expressed formally as follows: T[~(lambda (v1... Vm) e l . . . en)~ = (let/cc ([c :cc:]) (let/cc ([frame (let/cc ([entry :cc:]) (c entry))]) T ~ e ~ . . . ((list-ref frame 0) T~e'n~))) where e~ = 6i[(list-ref frame 1). . . . . (list-ref frame m)/v~ . . . . . Vm] is the expression obtained from fi~ by simultaneously substituting (list-ref frame j) for each free occurrence of vj in ei, and 6~ = e~{set-car!, (list-tail frame 1). . . . . (list-tail frame m)/set!, v~. . . . . Vm} is the result of simultaneously substituting set-car! for each (free) occurrence of set! that modifies some free occurrence of vj in e~, and (list-tail frame j) for each free occurrence of vj in e~ that is modified by a set!.t The variable frame should not occur free in each e~. Observe closely that this condition implies that if an e~contains a lambda abstraction in which some vj occurs free then, by recursively applying this condition, a new name of the frame must be used in the transformed code of e~ in order to avoid capturing the variable frame in the transformed code of the enclosing lambda abstraction being transformed. This feature makes (nonlocal) variable referencings easy--there is no need to look for the frames in which the values of the variables reside. Needless to say, this variation makes only one copy of the frame and is still functional (in case the source code is functional, of course), at a price of losing readability. But, perhaps it is more fWe adopt brackets to denote the usual substitutions used for the generalized r-conversion in the 2-calculus [9], and braces to denote substitutions similar to the usual ones but with some extra conditions.
30
CHING=LIN WANG
or less resemblant to assembly-language programming in that formal parameters of a function are anonymous and referenced by means of index addressing. It is interesting to note that within this variation the only meaningful use of the assignment operator set! is to modify global variables. Modifications of nonglobal variables using set! originally have been consistently replaced by the mutator set-car!. The implication of this is that we have (partially) shown that assignment can be implemented in terms of mutation without the need to look at the concrete or metacircular evaluator. (For a discussion on the equivalence of assignment and mutation, see [10, pp. 207-208].) Manipulating the static chain. So far, we have only been able to talk about the frame; the whole run-time stack has been left behind the scenes. In what follows, we are about to let the run-time stack enter on the scene. In order to manipulate the run-time stack, a lambda abstraction will not be transformed into a continuation alone; rather it will be transformed into a closure. As is well-known, a closure is an object consisting of two parts: the code of a lambda abstraction and the (enclosing) environment in which the lambda abstraction was evaluated. The idea here is not new. What is new is that the code part of a closure will be represented as a continuation. By choosing different continuations to represent the code part of a closure, we may manipulate the run-time stack in two ways: to maintain the static chain [19, pp. 246-250] explicitly and the dynamic chain [19, pp. 184-187] implicitly as in a metacircular evaluator for Scheme or to maintain both chains explicitly as in the usual implementations of block-structured languages. We shall explore them, in that order, in the sequel. Before embarking upon the job, recall that in a block-structured language a reference to a variable may be compiled into a lexical address (f, d), where f is the frame number and d the displacement number [10, p. 486]. For the purposes of discussion, we assume that every reference to a nonglobal variable (i.e. a formal parameter of some lambda abstraction) has already been compiled into a lexical address. We are now ready to show how to make the static chain explicit. Let us consider function applications first. Assuming that closures are represented by pairs of the form ((continuation). (enclosing environment)), we shall transform function applications as follows (assuming for simplicity that e I evaluates to a lambda function):
X (e, e2... en)] = (let/cc ([fun T~el ~] [ret :cc:]) ((car fun) (cons (list ret T~e2~ ... T[en]]) (cdr fun)))) The effect of this transformation is to invoke the continuation that represents a lambda abstraction with the extended environment. The body of the lambda abstraction should then be evaluated in the context of the extended environment. To cope with this, lambda abstractions should be transformed as follows: T~(lambda ( v , . . . Vm) e l . . . en)] = (let/cc ([c :cc:]) (let/cc ([:env (let/cc ([entry :cc:]) (c (cons entry :env)))]) T [ e ~ ] . . . ((list-ref (car :env) 0) TilerS))) Notice that, of the two occurrences of the variable :env in the second line of the transformed code, the right one stands for the enclosing environment and the left one the extended environment. The important point to observe here is that, unlike in the anonymous-formal-parameters transformation, the symbol :env should be used to name the (enclosing and extended) environments throughout the transformations of lambda abstractions included in the eis, because the enclosing environment of a lambda abstraction contained in an el is precisely the extended environment of the enclosing lambda abstraction. The variable :env must also exist globally, initializing to an empty list and serving as the enclosing environment of top-level lambda abstractions. A consequence of this is that variables cannot be easily accessed as in the anonymous-formalparameters transformation--we must look for the frames in which the values of the variables reside. So, suppose the lexical address of a reference to the R-value of some vj in an ei is (fj, j), then we should substitute (list-ref ( c a d . . . dr :env) j) (or, (list-tail ( c a d . . . dr :env) j) for accessing the
A continuation-based language embedded in Scheme
31
L-value) with fj cdr operations for that occurrence of vj. The exact definitions of the e{s follow directly from this description. Observe that the cdr chain of an environment serves as the static chain for variable referencings. On the other hand, there is no explanation of how the stack grows and shrinks, or equally, how the dynamic chain is maintained, as the transformed code inherits the control structure of the underlying Scheme-21 system (or the underlying Scheme system if the former is embedded in the latter). Ignoring Scheme-21 for the moment, it should have no doubt that this transformation operates the run-time stack in the fashion of a metacircular evaluator for Scheme (e.g. [10, Section 4.1]). In fact, based on this transformation, we could obtain a new, continuation-based metacircular evaluator for Scheme [20]. By the way, the use of lexical addressing here is merely to streamline the presentation and is not essential. It is a simple matter to use deep binding [10, p. 309] instead by carrying, in the usual way, the names of the formal parameters in a closure. Manipulating the static and dynamic chains. To make both the static and dynamic chains explicit, the structure of frames will be reformatted as ((static chain pointer)(return address) (values of actual parameters)) where the static chain pointer is just the pointer to the enclosing environment carried by a closure. Aside from a lack of the dynamic chain pointer, this structure should remind one of that used in the usual implementations of block-structured languages. Let us suppose again that closures are represented by pairs of the form ((continuation) .(enclosing environment)); function applications will now be transformed as follows: T~(el e2... en)~ = (let/cc ([fun T[[el ~] [ret :cc:]) ((car fun) (list (cdr fun) ret T~e2~... T[[e,~))) The effect is to invoke the continuation that represents a lambda abstraction with a new frame. This frame should be pushed onto the stack in effect at the time of the call. The body of the lambda abstraction should then be evaluated in the context of the new stack constructed, using the static chain starting from the top frame for variable referencings. On returning the result to the caller, the stack should be restored by popping the top frame off. All these actions are incorporated in the following transformed code of lambda abstractions: T~(lambda ( v l . . . Vm) e l . . . en)~ = (let/cc ([c :cc:]) (set! :stack (cons (let/cc ([entry :cc:]) (c (cons entry :stack))) :stack)) T~e~... (let/cc ([ret (list-ref (car :stack) 1)] [val T~e;~]) (set! :stack (cdr :stack)) (ret val))) The global variable :stack has as its initial value an empty list. Observe that, among the three occurrences of the variable :stack in the second line of the transformed code, the middle one stands for the enclosing environment, the right one the stack before the call, and the left one the stack after the call. Observe also that the cdr chain of the stack now serves as the dynamic chain for maintaining the stack. The last thing remained to be specified is for variable referencings. Given that the lexical address of a reference to the R-value of some vj in an ei is (fj, j + 1), we should substitute (list-ref ( c a . . . ar :stack) j + 1) (or, (list-tail ( c a . . . a r : s t a c k ) j + 1) for accessing the L-value) with 2fj+ 1 car operations for that occurrence of vj. Imagining at this point that Scheme-21 is an implementation language for Scheme, the transformed code of lambda abstractions described above should be reminiscent of the traditional implementations of functions in block-structured languages: A function is compiled into a block of code--beginning with a prologue to set up the frame, ending up with an epilogue to release the
32
CI.tlNGoLIN WANG
storage for the frame and to return result [19, p. 135], sandwiching with the actual code for the function in which assignments are replaced by mutations, named formal parameters become anonymous, and references to variables are accessed along the static chain--which is identified by an entry point, pointing to the start of the prologue. All these are, once again, not new. What is new is our account of these hidden concepts in terms of continuations. The register method. In all of the preceding transformations, the return address of a function application is passed in a direct manner to the function in question. A slight variation is to pass, in isolation, the return address through a global variable, say :return. This style of transformations will be referred to as the register method (viewing :return as a register), while the original style will be referred to as the frame method. Each of the transformations of the frame method has a counterpart of the register method. We shall, however, restrict ourselves to the counterpart of the functional-style transformation of the frame method. The resulting transformation is easily obtained. For function applications, we simply replace part of the transformed code (fun (list ret arg2.., arg,)) by a sequence of two expressions (set! :return ret) (fun (list arg2.., arg,)) And, for lambda abstractions, we substitute :return for (list-ref frame 0) and (list-ref frame i-l) for (list-ref frame i) for all i in 1 to m. An important property of the register method holds for unary functions if we simplify the transformation for this special case as follows: T~(lambda (v) e~... e,)~ - ( l e t / c c [(c :cc:]) (let/cc ([v (let/cc ([entry :cc:]) (c entry))]) (let/cc ([ret :return]) Tie ' ] . . . (ret Tie, I))))
T (e,
---(let/cc ([fun Tl[e~]]] [arg T~e,~] [ret :cc:]) (set! :return ret) (fun arg)) The essential property of this transformation, which is not shared by those of the frame method and the general one of the register method just described, is that there is no need for type checking in the transformed code of a unary function application, because the same calling convention can be used whether eL evaluates to a primitive or lambda function, even though in the former case the transformed code is somewhat clumsy. Moreover, even if e~ evaluates to a continuation function this transformation is still applicable for the same reason. We shall soon take advantage of this fact in transforming call/cc. A final point is worthy of note. It might be tempting to get rid of the two local variables fun and arg in the transformed code of unary function applications (because T~el]] and T~e:]] each appear only once), but that would not be correct unless eL and e2 include no unary function applications (so that if e~ evaluates to a lambda function, the return address stored in :return will not be destroyed before it is captured by the transformed code of e~).
4.3. Transforming call/ce We now proceed to consider the transformation of caU/cc. Clearly, call/cc must be transformed into let/cc in one way or another. But there are several difficulties: • call/cc, being a primitive function, is a first-class object in Scheme, whereas let/cc, being a syntactic form, is not a first-class object in Scheme-2v • Any unary function, whether denoted by a lambda expression (which includes explicitly a variable to hold a continuation) or not, can be used as an argument of call/cc, whereas let/cc, when used to grab continuations, needs explicit variables.
A continuation-basedlanguage embedded in Scheme
33
• There are now two kinds of continuations--those that represent lambda abstractions and those that do not--in Scheme-21, but which are indistinguishable using any of the three type predicates for functions. It is, however, surprisingly easy to resolve these difficulties. The first one can be overcome by the trick introduced earlier to deprive call/cc of its first-class nature. In this context, the required transformation is: T~v ~ - T[[(lambda (f) (v f))~, if v - call/cc; - v, otherwise. To surmount the second one we simply coerce, if need be, the argument of call/cc into an equivalent lambda expression: T~(call/cc e)~ - T~(cali/cc (lambda (k) (e k)))~, if e is not a lambda expression; = (let/cc ([k :cc:]) T ~ e ~ . . . T~e~), if e - (lambda (k) e~... en) As for the last difficulty, the register method which we just introduced can be employed to transform unary lambda abstractions and function applications so as to by-pass the unconquerable type checkings. Finally, the transformation of applications of call/cc is assumed to take higher precedence than that of other unary function applications.
5. IS SCHEME-21 E Q U I V A L E N T TO SCHEME? We now come to the question: is Scheme-)~l equivalent to Scheme? Evidently, the answer depends on whether applications of the type predicates for functions in Scheme can be transformed into equivalent ones in Scheme-21. First of all, the predicate procedure? presents no difficulties. Applications of procedure? in Scheme can be transformed in the usual way by treating them as unary function applications, as they are meant to differentiate functional from non-functional objects. The problem arises from the presence of the predicates continuation? and closure?. The real trouble here is that lambda abstractions are of the type closures in Scheme, but their representations in Scheme-21 are of the type continuations (or of the form ((continuation). (environment))). In order to say that the two languages are equivalent, we must somehow or other preserve the type properties of lambda abstractions. It might seem that this is impossible, but as it is, the problem here is merely a representation issue: Conceiving again that Scheme-2~ is an implementation language for Scheme, the usual representation techniques could be employed to represent a lambda abstraction as, say (:closure. (continuation)), which is then interpreted as a closure, rather than a continuation or a pair. The tag :closure serves as a type descriptor and is internal to the implementation. The original tagless object code obtained by the frame method (say the functional-style transformation) plays the part of (continuation). (The register method is no longer needed, as can be seen.) Nonetheless, when we construct the embedding later on, we shall still restrict ourselves to the original representation for a couple of reasons. For one thing, it seems obvious that if embedded otherwise there will be no way to prevent the tagged object code from being intentionally manipulated as a pair, and to preclude the type tag itself from being deliberately used to cheat the type predicates for functions of Scheme-21 (which need revising so as to accommodate themselves to the new representation). For another thing, it seems that restricting to the original representation will not make the embedded Scheme-2~ far less expressive then Scheme. In view of the fact that the distinction between closures and continuations is only an artifact of implementations, we wonder whether there are any meaningful Scheme programs that call for closure? or continuation? to differentiate closures from continuations, not to speak of whether they can be expressed in the embedded Scheme-2~ or not. [N.B. Although all functions in Scheme could be treated as a single type, the same is not true in Scheme-21--we must treat primitives and continuations as distinct types, for obvious reasons. CL 1 7 / 1 ~
34
CHING-LIN WANG (extend-syntax (:lambdSr) [(:lambda r (v) el e2 ...) (with ([c (gensym)] [ret (gensym)] [(exp ...) (drop-last--pair '(el e2 ...))] [(last--ex-p) (last-pair '(el e2 ...))]) (let/cc ([c:cc:])
(let/cc ([v (let/co ([entry :co:])(c entry))]) (let/cc ([ret :return]) exp ... (ret last--exp)))))] [(:lambda r (v ...) el e2 ...) (with ([c (gensym)] [frame (sensym)] [ret (sensym)l [(indexO ...) (gen-index (length '(v ...)))] [(exp ...) (drop-last-pair '(el e2 ...))]
[(last--exp)(last--pair'(ele2 ...))]) (let/co ([c :co:]) (let/cc ([frame (let/cc ([entry :cc:]) (c entry))]) (let/cc ([ret :return] Iv (list-tel flame indexO)] ...)
(extend----eynt ax (:cldlr) [(:call r el e2) (let/cc ([fun el] [azg e2] [ret :cc:]) (set! :return let) (fun arg))] [(:c~ r el e2 ...) (with ([(azg ...) (rasp geasym '(e2 ...))])
(let/cc ([tim eli [ass e21 ...) (if (not (continuation? fun)) ( ~ a~S ...) (l~/cc ([~t :cc:D
e~p ... (~etlut-e~)))))])
(set! :return ret) (fun (list ~g ...))))))])
Fig. 3.(a) The macro :lambdar.
Fig. 3.(b) The macro :call r
Thus, if one prefers treating all functions in Scheme as a single type (to which we do not object), then he is obliged to supply Scheme-2t with some type predicates for functions, say :primitive? or :continuation? or both. But, this does not amount to say that Scheme-2t must be redefined to include them as primitives. On the contrary, :primitive? can be defined, using the primitive eq? for membership tests, as a continuation in Scheme-21; and :continuation? is just a negation of :primitive?. There is, however, one thing that needs to be redefined, namely, the transformation of function applications, since the call of continuation? in the transformed code is no longer legitimate.] 6. C U S T O M I Z I N G
S E V E R A L U S E F U L M A C R O S F O R SCHEME-21
In this section, we shall customize several useful syntactic extensions for Scheme-2~. The purposes of these macros are to facilitate programming in Scheme-2~; to realize the foregoing transformations in a simple manner; to clarify the frame and register methods; and to prelude the next section. The macros we shall introduce are :lambda, and :call, for the register method and :lambdaf and :callf for the frame method. Figure 3(a) and (b) present the implementations of :lambda, and :call,, respectively. The expansion parts of both figures are essentially the transformed code described earlier. In particular, we adopt the simplified code for unary lambda functions and function applications. The other pair :lambdaf and :callr could be implemented in a number of ways corresponding to those described in Subsections 4.1 and 4.2, and will not be presented here. Observe that the use of uninterned symbols in defining :lambda, of Fig. 3(a) is indispensable in order to avoid capturing free variables occurred in the body expressions of :lambda, abstractions. But the use of uninterned symbols in defining :call, of Fig. 3(b) is not really necessary as no captures can ever occur. The function last-pair has its usual meaning; the meanings of drop-last-pair and gen-index can be easily inferred. Figure 4 offers two simple uses of the macros :lambda, and :call,. Figure 4(a) defines a syntactic extension :let for Scheme-21, whose counterpart in Scheme is let. The reader is encouraged to fully expand the expansion part of Fig. 4(a) into let/cc (or lambda and call/cc) in order to appreciate its neatness. (This example is for illustration only as Scheme-2~ does not need the macro :let, for obvious reasons.) Figure 4(b) defines a :lambda, abstraction :call/cc for Scheme-2~, which is the realization of Scheme's call/cc. Although :call r was not presented here, we must note that :callr and :callr, when used to call unary functions, are asymmetric in the sense that only :call, can be used to call continuations that do not represent lambda abstractions. Thus, for example, in Fig. 4(b) the :call r should not be replaced by a :call r or else continuations that do not represent lambda abstractions cannot be passed as
A continuation-based language embedded in Scheme
(extend---syntax(:let) [(:let (Iv el ...) expl exp2 ...) (:callr (:lambdar (v ...)expl exp2 ...)e ...)]) Fig. 4.(a) The macro :let.
35
(define :c~/cc (:l~bd~, (0 (let/cc ([it :ee:]) (:call, f k)))) Fig. 4.(b) :call/cc as a :lambda r abstraction.
downward funargs to :call/cc. This in turn implies that :call/cc should not be defined as a :lambdaf abstraction or else it could not be passed a downward funarg, e.g. the application (:call/cc :call/cc).t On the other hand, the pair :callr and :lambda, in Fig. 4(a) could very well be replaced by :callf and :lambdar, independent of the number of the [variable value] pairs. Due to this asymmetry between :call, and :callf, programming in Scheme-2~ should be done with some care. As a rule implied by the preceding examples, unary lambda abstractions and function applications should in general be coded by the register method. As for non-unary ones, it does not matter which of the two methods is used. In the next section where we construct the embedding we shall arbitrarily adopt the convention that uses the register method for unary ones, and the frame method for non-unary ones. 7. E M B E D D I N G SCHEME-21 IN SCHEME As mentioned earlier, realizing let/cc as a syntactic extension results only in a partial embedding of Scheme-2~ in Scheme in case the macro facility is also taken into consideration. To see this, we simply notice that Scheme-2~ must have its own macro processor, which should, for consistency, operate with expansion functions written in Scheme-2l rather than in Scheme. The macro processor, say :expand, of Scheme-21 could be written in Scheme, but we choose to do it in Scheme-2~ instead, serving as another example of programming in Scheme-21. The result is shown in Fig. 5. The function :map is to Scheme-2~ what the function map is to Scheme; so does the macro :cond. The function :expand-val-exps takes as its input a list of pairs ([v~ e l ] . . . [Vm era]) and returns as its value ([v~ e'l]... [Vm e~]), where e~ is the result of evaluating Ccallr :expand ei). The :lambdar abstraction :expand of Fig. 5 is essentially a direct transformation of the lambda abstraction expand of Scheme [13, p. 56]. Apart from the lines for handling let/cc, the only significant difference lies in the line Ccallr :expand (:callr (macro-function (car x)) x)) This allows the expansion functions of syntactic extensions to be any unary functions, except for unary :lambda r abstractions, in Scheme-2~. (This restriction is unfortunate. But if :callr were used instead then it would be even more limited, as explained earlier.) To make the embedding complete, we provide below in Fig. 6 a simple user interface for Scheme-21. (define :expand (:lambda r (x)
(:cond ((not (pair?x)) x) ; constants, identifiersand :cc: ((macro? (carx)) (:call,:expand (:callr (macro-function (car x)) x))) ((eq? (car x) 'quote) x) ((or (eq? (carx) 'define) (eq? (coxx) 'set!)) '(,(carx) ,(caxlr x) ,(:call r :expand (caddr x)))) ((eq? (earx) 'if) '(if,O(:callf:map :expand (cdr x)))) ((eq? (carx) 'let/cc) '(let/cc,(callr :expand-vai-exps (cadr x)) ,Q(:callf:map :expand (cddr x)))) (else(:cant,:map :expand x))))) Fig. 5. The macro processor :expand of Scheme-2 I. tSee [6, p. 559].
(define :interface (:lsmbdst 0 (display ": ")
(mite (evai(:can, :~,~d (re~t)))) (newline) (:callf :interface))) Fig. 6. The user interface.
36
CHING-LINWANG
Notice that in Fig. 6 we take advantage of Scheme's evaluator eval to evaluate the macro-free Scheme-2j expressions. This works because an invocation of Scheme's macro processor e x p a n d - - t o expand a macro-free Scheme-2~ expression into a macro-free Scheme expression--will occur automatically in any reasonable Scheme system. O f course, the macro-free Scheme-21 expressions could have been evaluated by their own evaluator. But this demands the evaluator to exist--which is what we are trying to avoid. Nevertheless, it is worth while mentioning that a continuationsemantics, metacircular evaluator of Scheme-2t could be obtained in the embedding with moderate efforts by transforming for the most part that of Scheme [5, 7]. The resulting evaluator would be truly metacircular, as opposed to that of Scheme in which metacircularity is not used in defining call/cc [5, p. 147]. Finally, to start up the system, one needs only invoke (:callr :interface)
8. C O N C L U S I O N Continuations in Scheme were usually utilized to obtain advanced control structures. In this paper, we have taken an opposite approach to show that they could also be used to simulate and elucidate the low-level control structures underlying the implementations of block-structured languages. By representing lambda abstractions as continuations, a number of programminglanguage concepts related to the low-level control structures, which are usually sensible only to assembly-language programmers, have appeared on the scene. The fact that continuations in Scheme could be manipulated in such picturesque ways to make those hidden concepts vivid is, we believe, a contribution of this paper. Being able to represent lambda abstractions, continuations could serve as the basis of a Scheme-like language, called Scheme-2~, which is essentially a constrained subset of Scheme in which lambda degenerates into a block constructor. Although being a subset, Scheme-2~ turns out to be equivalent to Scheme. This is indeed a natural consequence of the fact that it m a y be conceived as an implementation language for Scheme. We have explained this by demonstrating how to t r a n s f o r m - - o r rather, c o m p i l e - - l a m b d a abstractions in Scheme into equivalent object code in Scheme-2t. We have also introduced another continuation-based language, called Scheme-22, which is equivalent to Scheme too; but we have left it out of consideration by reason of its extreme inefficiency. Towards the end of representing every lambda abstraction as a continuation, our work would not be complete if the macro facility were not taken into consideration. For that purpose, we have constructed an embedding of Scheme-2~ in Scheme by realizing the core syntactic form let/cc of Scheme-21 as a syntactic extension in Scheme and providing Scheme-2~ with its own macro processor and user interface. Acknowledgements--I would like to express my heartfelt gratitude to Professor Louis R. Chow at Tamkang University for
his encouragement throughout this work. My thanks also go to the anonymous referee for the detailed, valuable comments on an early draft of the paper.
REFERENCES 1. Wang, C. L. Obtaining lazy evaluation with continuations in Scheme. Inform. Process. Lett. 35: 93-97; 1990. 2. Clinger, W. and Rees, J. (Eds) The revised report on the algorithmic language Scheme. SIGPLAN Notice 21: 37-79; 1986. 3. Steele, G. and Sussman, G. Scheme: an interpreter for extended lambda calculus. MIT Artificial Intelligence Memo 349; 1975. 4. Wand, M. Continuation-based program transformation strategies. J. ACM 27: 164-180; 1980. 5. Haynes, C. T., Friedman, D. P. and Wand, M. Obtaining coroutines with continuations. Comput. Lang. I1: 143-153; 1986. 6. Springer, G. and Friedman, D. P. Scheme and the Art of Programming. Cambridge, Mass./New York: MIT Press/McGraw-Hill; 1989. 7. Friedman, D. P., Haynes, C. T. and Kohlbecker, E. Programming with continuations. In Program Transformation and Programming Environments (Edited by Pepper P.), pp. 263-274. New York: Springer; 1984. 8. Haynes, C. T. Logic continuations. J. Logic Progr. 4: 157-176; 1987. 9. Gordon, M. J. C. Programming Language Theory and its Implementation. Prentice-Hall Int.; 1988.
A continuation-based language embedded in Scheme
37
10. Abelson, H. and Sussman, G. J. Structure and Interpretation of Computer Programs. Cambridge, Mass.: MIT Press; 1985. 11. Franco, F., Friedman, D. P. and Johnson, S. D. Multi-way streams in Scheme. Comput. Lang. 15: 109-125; 1990. 12. Felleisen, M. Reflections on Landin's J-operator: a partly historical note. Comput. Lang. 12: 197-207; 1987. 13. Dybvig, R. K., Friedman, D. P. and Haynes, C. T. Expansion-passing style: a general macro mechanism. Lisp Symbolic Comput. 1: 53-75; 1988. 14. Dybvig, R. K. The Scheme Programming Language. Englewood Cliffs, N.J.: Prentice-Hall; 1987. 15. Friedman, D. P., Haynes, C. T., Kohlbecker, E. and Wand, M. Programming Languages: Abstraction, Representation, and Implementation. Cambridge, Mass./New York: MIT Press/McGraw Hill; 1990. 16. Texas Instruments Inc. TI Scheme Language Reference Manual, Revision B; 1987. 17. Friedman, D. P., Haynes, C. T., Kohlbecker, E. and Wand, M. Scheme 84 Interim Reference Manual. Technique Report No. 153, Computer Science Department, Indiana University; 1985. 18. Wand, M. and Friedman, D. P. Compiling lambda-expressions using continuations and factorizations. Comput. Lang. 3: 241-263; 1978. 19. Pratt, T. W. Programming Languages: Design and Implementation, 2nd edn. Englewood Cliffs, N.J.: Prentice-Hall; 1984. 20. Wang, C. L. A new, continuation-based metacircular evaluator for Scheme. In preparation. About the Autbor-4Shing-Lin Wang received her B.A. degree from Tamkang University in 1975 and her M.S. degree in computer science from Indiana University in 1983. She has been on the faculty of the Department of Computer Science at Tamkang University since 1984. Her current interests include programming languages in general, and Scheme in particular.