Analysis of information sources in references of the Wikipedia article "Scheme" in Chinese language version.
(%A M%)
to indicate sending the message M to the actor A. We shall use [s1 s2 … sn]
to denote the finite squence s1, s2, … sn. ……=
. ……(=> pattern body)
is an abbreviation for (receive(message pattern) body)
where receive is a more general actor that is capable of bindinq elements of the action in addition to the message.(%(=> pattern body) the-message%)
, i.e. sending(=> pattern body) the-message
,NOT-APPLICABLE
to the-message. If the-message matches pattern, the body is evaluated.(send T
(message M
[#continuation C]))
The transmission (%T M%)
is an abbreviation for the above where C is defaulted to be the caller. If target T is the following:
(receive
(message the-body
[#continuation =the-continuation])
the-body)
then the the-body is evaluated in an environment where the-message is bound to M and the-continuation is bound to C.
……
Many actors who are executing in parallel can share the same continuation. They can all send a message [“return”]
to the same continuation. ”
Irene Greif, Carl Hewitt. Actor semantics of PLANNER-73. 1975 [2022-11-15]. (原始内容存档于2022-11-15).
We concluded that actors and closures were effectively the same concept. (Hewitt later agreed with this, but noted that two types of primitive actors in his theory, namely cells (which have modifiable state) and synchronizers (which enforce exclusive access), cannot be expressed as closures in a lexically scoped pure Lisp without adding equivalent primitive extensions.)
In summary, Sussman and Steele [1975] mistakenly concluded “we discovered that the 'Actors' and the lambda expressions were identical in implementation.” The actual situation is that the λ-calculus is capable of expressing some kinds of sequential and parallel control structures but, in general, not the concurrency expressed in the Actor model. On the other hand, the Actor model is capable of expressing everything in the λ-calculus and more [Hewitt 2008f].
Also we represent the value of a λ-expression by a bundle of information called a "closure," comprising the λ-expression and the environment relative to which it was evaluated. We must therefore arrange that such a bundle is correctly interpreted whenever it has to be applied to some argument. More precisely:
a closure has
an environment part which is a list whose two items are:
⑴ an environment
⑵ an identifier or list of identifiers,
and a control part which consists of a list whose sole item is an AE.
The value relative toE
of a λ-expressionX
is represented by the closure denoted by
constructclosure((E, bvX), unitlist(bodyX))
Scheme is a statically scoped and properly tail-recursive dialect of the Lisp programming language invented by Guy Lewis Steele Jr. and Gerald Jay Sussman. It was designed to have an exceptionally clear and simple semantics and few different ways to form expressions. A wide variety of programming paradigms, including imperative, functional, and message passing styles, find convenient expression in Scheme.
Inspired by ACTORS [Greif and Hewitt][Smith and Hewitt], we have implemented an interpreter for a LISP-like language, SCHEME, based on the lambda calculus [Church], but extended for side effects, multiprocessing, and process synchronization. The purpose of this implementation is tutorial. We wish to:
⑴ alleviate the confusion caused by Micro-PLANNER, CONNIVER, etc. by clarifying the embedding of non-recursive control structures in a recursive host language like LISP.
⑵ explain how to use these control structures, independent of such issues as pattern matching and data base manipulation.
⑶ have a simple concrete experimental domain for certain issues of programming semantics and style.
The Revised7 Report on the Algorithmic Language Scheme (“R7RS”) intentionally describes a small language, suitable for implementing on reduced hardware, or for experiments in programming language design and implementation.
However, Scheme is used for practical programming, and there a minimal language is not a help. Additional data structures and programming idioms make the programmer’s job easier (if they are well-designed!). Therefore, as part of the R7RS design process, it was agreed that a subsequent report would appear on “R7RS-Large”, a collection of Scheme libraries that are useful across a wide variety of problems.
The decision of what was to appear in R7RS-Large was left to the community, and a strategy was adopted: proposals were published as SRFIs (Scheme Requests For Implementation, http://srfi.schemers.org), and the community would then vote on which would be adopted.
As a result, R7RS-Large will be developed in several stages, depending upon the support from the community for each proposal. At each stage, several SRFIs are frozen, and implementers and users may then rely upon those libraries being in the final product. (In a few cases, a library might be revisited, with a subsequent stage adopting a completely backward-compatible new version of that library; this has already happened with generators.)
When this process has completed, the resulting interim reports will be collated and reorganized into a final document.
In most current Lisp systems the expander's task is confined to the process of finding syntactic extensions and replacing them by their expansions. This implies, in particular, the each macro function is responsible for the integrity of the program. For Lisp systems (and other languages with similar macro facilities) this means specifically that variable binding must not be corrupted. This, however, is not as simple a task as it sounds. ……
The real danger of these erroneous macros is that they are treacherous. They work in all cases but one: when the user - or some other macro writer - inadvertently picks the wrong identifier name.
Various techniques have been proposed to circumvent this capturing problem, but they rely on the individual macro writer. if even one of many macro writers is negligent, the macro system becomes unsafe. We claim that the task of safely renaming macro-generated identifier is mechanical. It is essentially an α-conversion, which is knowledgeable about the origin of the identifiers. For these reasons we propose a change to the navïe macro expansion algorithm which automatically maintains hygienic conditions during expansion time.
In most current Lisp systems the expander's task is confined to the process of finding syntactic extensions and replacing them by their expansions. This implies, in particular, the each macro function is responsible for the integrity of the program. For Lisp systems (and other languages with similar macro facilities) this means specifically that variable binding must not be corrupted. This, however, is not as simple a task as it sounds. ……
The real danger of these erroneous macros is that they are treacherous. They work in all cases but one: when the user - or some other macro writer - inadvertently picks the wrong identifier name.
Various techniques have been proposed to circumvent this capturing problem, but they rely on the individual macro writer. if even one of many macro writers is negligent, the macro system becomes unsafe. We claim that the task of safely renaming macro-generated identifier is mechanical. It is essentially an α-conversion, which is knowledgeable about the origin of the identifiers. For these reasons we propose a change to the navïe macro expansion algorithm which automatically maintains hygienic conditions during expansion time.
To use functions as arguments, one needs a notation for functions, and it seemed natural to use the λ-notation of Church (1941). I didn’t understand the rest of his book, so I wasn’t tempted to try to implement his more general mechanism for defining functions. Church used higher order functionals instead of using conditional expressions. Conditional expressions are much more readily implemented on computers.
LISP was not based on the lambda calculus, despite using the word “LAMBDA” to denote functions. At the time he invented LISP, McCarthy was aware of (Church 1941) but had not studied it. The theoretical model behind LISP was Kleene’s theory of first order recursive functions. (McCarthy made these statements, or very similar ones, in a contribution from the floor at the 1982 ACM symposium on LISP and functional programming in Pittsburgh. No written version of this exists, as far as know.)
In the interpreter “variables” are implemented as atomic symbols which possess “shallow-bound” value cells. The continual manipulation of value cells would decrease the efficiency of compiled code, so the compiler defines two types of variables: “special variables” and “local variables.” Special variables are identical to variables in the interpreter.
Local variables are more like the variables in commonly-used algebraic programming languages such as Algol or PL/I. A local variable no longer has an associated atomic symbol; thus it can only be referred to from the text of the same function that bound it. The compiler creates local variables forPROG
variables,DO
variables, andLAMBDA
variables, unless directed otherwise. The compiled code stores local variables in machine registers or in locations within a stack.
The principal difference between local variables and special variables is in the way a binding of a variable is compiled. (A binding has to be compiled when aPROG
,DO
, orLAMBDA
expression is compiled, and for the entry to a function which hasLAMBDA
variables to be bound to its arguments.) If the variable to be bound has been declared to be special, the binding is compiled as code to imitate the way the interpreter binds variables: the value of the atomic symbol is saved and a new value is stored into its value cell. If the variable to be bound has not been declared special, the binding is compiled as the declaration of a new local variable. Code is generated to store the value to which the variable is to be bound into the register or stack-location assigned to the new local variable. This runs considerably faster than a special binding.
Although a special variable is associated with an atomic symbol which has the name of the variable, the name of a local variable appears only in the input file - in compiled code there is no connection between local variables and atomic symbols. Because this is so, a local variable in one function may not be used as a “free variable” in another function since there is no way for the location of the variable to be communicated between the two functions.
When the usage of a variable in a program to be compiled does not conform to these rules, i.e. it is somewhere used as a “free variable,” the variable must be declared special. There are two common cases in which this occurs. One is where a “global” variable is being used, i.e. a variable which isSETQ
'ed by many functions but is never bound. The other is where two functions cooperate, one binding a variable and then calling the other one which uses that variable as a free variable.
The use of free variables is a very powerful computational idea. In many situations it is possible to avoid using free variables by supplying a function with additional arguments. This can be quite cumbersome. ……
When a function uses free variables or when it calls functions which use them, then the value of this function is not completely determined by its arguments, since the value might depend on the values of the free variables. Thus in order to completely specify a computation, one has to specify a function and its arguments in addition to an environment in which the values of the free variables can be determined. ……
An important point which must be realized about functional arguments (abbreviated FUNARGs) is that two different environments are involved in such cases. The first environment is the one which is in effect when then FUNARG is bound as an argument. We shall call this the binding environment. The second environment is the one that is in effect when the FUNARG is activated as a function. We shall call this the activation environment. ……
Consider now what it would require for the system to restore the binding environment for functional arguments. It would require knowing where in the stack or "alist" the binding environment exists through some pointer to it. Supplying such pointer is a functionFUNCTION
in LISP. That is when one transmits a functional argumentf
which is to be evaluated in its binding environment, then one usesFUNCTION(f)
.FUNCTION
will prevent its argumentf
from being evaluated, just asQUOTE
would. The result ofFUNCTION
will be a structure which not only contains a reference to the functionf
, but also contains a pointer to the binding environment. Thus at the FUNARG's activation time we will be able to use the current place in the stack up to the point where the binding environment exists, thus skipping over any values in the activation environment which differ from those in the binding environment. ……
The points we have made so far are:
1) Free variables in function definitions require that one must have an environment in order to be able to evaluate a function.
2) When one uses functional arguments, the the question arises as to which environment one chooses in order to evaluate the function, the binding environment or the activation environment.
3) When one returns functions (which require the values of free variables for their evaluation) then one must, in general, save the binding environment and thus create a stack which is, in fact, a tree. ……
LISP allows the user to choose between the binding environment and the activation environment by allowing a function to be bound withFUNCTION
or withQUOTE
.QUOTE
will force the activation environment to be used in evaluating the function. A useful metaphor for the difference betweenFUNCTION
andQUOTE
in LISP is to think ofQUOTE
as a porous or an open covering of the function since free variables escape to the current environment.FUNCTION
acts as a closed or nonporous covering (hence the term "closure" used by Landin). Thus we talk of "open" Lambda expressions (functions in LISP are usually Lambda expressions) and "closed" Lambda expressions. ……
My interest in the environment problem began while Landin, who had a deep understanding of the problem, visited MIT during 1966-67. I then realized the correspondence between the FUNARG lists which are the results of the evaluation of "closed" Lambda expressions in LISP and ISWIM's Lambda Closures.
SCHEME is a dialect of LISP. It is an expression-oriented, applicative order, interpreter-based language which allows one to manipulate programs as data. It differs from most current dialects of LISP in that it closes all lambda-expressions in the environment of their definition or declaration, rather than in the execution environment. This has the consequence that variables are normally lexically scoped, as in ALGOL. However, in contrast with ALGOL, SCHEME treats procedures as a first-class data type. They can be the values of variables, the returned values of procedures, and components of data structures. Another difference from LISP is that SCHEME is implemented in such a way that tail-recursions execute without net growth of the interpreter stack. The effect of this is that a procedure call behaves like a GOTO
and thus procedure calls can be used to implement iterations, as in PLASMA.
Data and procedures and the values they amass, Higher-order functions to combine and mix and match, Objects with their local state, the message they pass, A property, a package, the control of point for a catch - In the Lambda Order they are all first-class. One thing to name them all, one things to define them, one thing to place them in environments and bind them, in the Lambda Order they are all first-class. ……
Scheme shares with Common Lisp the goal of a core language common to several implementations. Scheme differs from Common Lisp in its emphasis upon simplicity and function over compatibility with older dialects of Lisp.
System-Provided Extensions - Some standard syntactic extension are provided by convenience in ordinary programming. They are distinguished from other magic words in that they are semantically defined in terms of others rather then being primitive {Note FEXPRs Are Okay by Us}. For expository purposes they are described here pattern-matching/production-rules kind of language. takes advantage of the definition of list notation:(A B C) = (A . (B . (C . NIL)))
. Thus the pattern(x . r)
matches(A B C)
, withx = A
andr = (B C)
. the ordering of the "productions" is significant; the first one which matches is to be used.
The variables in a lambda expression are dummy or bound variables because systematically changing them does not alter the meaning of the expression. Thusλ[[u;v];v2+u]
means the same thing asλ[[x;y];y2+x]
.
We shall sometimes use expressions in which a variable is not bound by a lambda. For example, in the function of two variablesλ[[x;y];xn+yn]
the variablen
is not bound. This is called a free variable. It may be regarded as a parameter. Unlessn
has been given a value before trying to compute with this function, the value of the function must be undefined. ……
When the compiler is called upon to compile a function, it looks for anEXPR
orFEXPR
on the property list of the function name. The compiler then translates this S-expression into an S-expression that represents a subroutine in the LISP Assembly Language (LAP). LAP then proceeds to assemble this program into binary program space. Thus anEXPR
, or anFEXPR
, has been changed to aSUBR
or anFSUBR
, respectively. ……
Free variables in compiled functions must be declared before the function is compiled. ……
A variable is bound in a particular function when it occurs in a list of bound variables following the wordLAMBDA
orPROG
. Any variable that is not bound is free. ……
When a variable is used free, it must have been bound by a higher level function. If a program is being run interpretively, and a free variable is used without having bee bound on a higher level, error diagnostic *A 8* will occur.
If the program is being run complied, the diagnostic may not occur, and the variable may have value NIL.
There are three types of variables in compiled functions: ordinary variables,SPECIAL
variables, andCOMMON
variables.SPECIAL
andCOMMON
variables must be declared before compiling. Any variable that is not declared will be considered an ordinary variable.
When functions are translated into subroutines, the concept of a variable is translated into a location where an argument is stored. If the variable is an ordinary one, then a storage location for it is set up on the push-down list. Other functions cannot find this private cell, making it impossible to use it as a free variable.SPECIAL
variables have the indicatorSPECIAL
on their property lists Following the indicator there is a pointer to a fixed cell. When this variable is bound, the old value is saved on the push-down list, and the current value is stored in theSPECLAL
cell. When it is no longer bound, the old value must be restored. When a function uses this variable free, then the quantity in theSPECIAL
cell is picked up.SPECIAL
variables are declared by using the pseudo-functionspecial[a]
, wherea
is a list of variable names. ……SPECIAL
variables are inexpensive and will allow free communication among compiled functions. They do not increase run time significantly.SPECIAL
variables cannot be communicated between the interpreter and compiled functions.COMMON
variables have the flagCOMMON
on their property lists; however, this is only used to inform the compiler that they areCOMMON
, and is not needed at run time.COMMON
variables are bound on an a-list by the compiled functions. When they are to be evaluated,eval
is given this a-list. This happens at run time.
The use ofCOMMON
variables will slow down any compiled function using them. However, they do provide complete communication between interpreted and compiled functions.COMMON
variables are declared bycommon[a]
, wherea
is a list of variable names. ……
LISP is designed in such a way that all functions for which the possibility of recursion can exist are in fact recursive. This requires that all temporary stored results related to the computation that is in progress be set aside when a piece of coding is to be used recursively, and that they be later restored. This is done automatically and need not be programmed explicitly.
All saving of temporary results in LISP is performed on a linear block of storage called the push-down list. Each set of stored data that is moved onto the push-down list is in a block labeled with its size and the name of the subroutine from which it came. Since it is in the nature of recursion that the first block to be saved is always the last block to be restored, it is possible to keep the push-down list compact.
There are several ways in which to maintain the values of free variables (and, thus the computational environment) in order to handle cases like the one above. All of the techniques used involve some increase in time. One approach makes it easy to access the value of a free variable is local. This approach is favored in recent implementations of LISP. We shall call it the "shallow access" approach.
Another approach is to make it relatively easy to enter and leave a block in which a free variable is bound, but relatively expensive to access a free variable. This approach is used in the classical "alist" implementation of LISP. Several Algol systems have opted for a similar approach. We shall call this the "deep access" approach. Both of these approaches allow one to modify the value of the variable as well as access it. The access time is approximately the same as modification time in both approaches.
Let us consider the "shallow access" approach first. By "shallow access" we mean that the value of a free variable can be obtained in a single fetch from memory. Since the current value may be stored in a difficult-to-determine location up in the stack, a special cell for its current value is used. In many recent LISP implementations this special values cell is stored as a property of the atom structure, and is unique to the free variable. In order to maintain the proper value in the special cell, extra work must be done every time a function or a block is entered in which the variable is an argument or is local. On entering such a function or block one usually saves the old value which is in the special cell on the stack alone with sufficient information to allow one to know from where the stack value came. On leaving such a function or block one must remember to store the value saved in the stack in the special cell.
The "deep access" approach forces one to search upward in the stack for the most recent value of the free variable. Such a search may be quite deep and slow if the free variable was bond vary far up in the stack. However, this approach has the advantage of avoiding the need for an extra special value cell. In addition, we shall see later that this approach sometimes allows one to use free variables with greater ease and flexibility than the "shallow access" approach. ……
Certain LISP systems use "deep access" in interpreted functions and "shallow access" in compiled functions. if free variables in compiled functions are declared to beCOMMON
, their values will be stored on the "alist". In this manner one can solve the environment problem. The cost is a very slow interpreter and "deep access" to free variables.
DEFINE
- This is analogous to the MacLISPDEFUN
primitive (but note that theLAMBDA
must appear explicitly!). It is used for defining a function in the "global environment" permanently, as opposed toLABELS
(see below), which is used for temporary definitions in a local environment.DEFINE
takes a name and a lambda expression; it closes the lambda expression in the global environment and stores the closure in the LISP value cell of the name (which is a LISP atom). ……LABELS
- We have decided not to use the traditionalLABEL
primitive in this interpreter because it is difficult to define several mutually recursive functions using onlyLABEL
. The solution, which Hewitt [Smith and Hewitt] also uses, is to adopt an ALGOL-esque block syntax:
(LABELS <function definition list> <expression>)
This has the effect of evaluating the expression in an environment where all the functions are defined as specified by the definitions list. Furthermore, the functions are themselves closed in that environment, and not in the outer environment; this allows the functions to call themselves and each other recursively.
Atoms which are not atomic symbols (identifiers) evaluate to themselves. Typical examples of such atoms are numbers, arrays, and strings (character arrays). Symbols are treated as identifiers or variables. They may be lexically bound by lambda-expressions. There is a global environment containing values initially have as their values primitive operations such as, for example,CAR
,CONS
, andPLUS
. SCHEME differs from most LISP systems in that the atomCAR
is not itself an operation (in the sense of being an invocable object, e.g. a valid first argument toAPPLY
), but only has one as a value when considered as an identifier.
Non-atomic forms are divided by the evaluator into two classes: combinations and "magic (special) forms". …… Magic forms are recognized by then presence of a "magic (reserved) word" in thecar
position of the form. All non-atomic forms which are not magic forms are considered to be combinations. The system has a small initial set of magic words; there is also a mechanism for creating new ones {Note FUNCALL is a Pain}.
A combination is considered to be a list of subforms. These subforms are all evaluated. The first value mub be a procedure; it is applied to the other values to get the value of the combination. There are four important points here:
(1) the procedure Position is always evaluated just like any other position. (This is why the primitive operators are the values of global identifiers.)
(2) the procedure is never "re-evaluated"; if the first subform fails to evaluate to a applicable procedure, it is an error. Thus, unlike most LISP systems, SCHEME always evaluates the first subform of a combination exactly once.
(3) the arguments are all completely evaluated before the procedure is applied; that is, SCHEME, like most LISP systems, is an applicative-order language. Many SCHEME programs exploit this fact.
(4) the argument forms (and procedure form) may in principle be evaluated in any order. This is unlike the usual LISP left-to-right order. (All SCHEME interpreters implemented so far have in fact performed left-to-right evaluation, but we do not wish programs to depend on this fact. Indeed, there are some reasons why a clever interpreter might want to evaluate them right-to-left, e.g. to get things on a stack in the correct order.)
BLOCK
- This is like the MacLISPPROGN
, but arranges to evaluate its last argument without an extra net control frame (explained later), so that the last argument may involved in an iteration. Note that in SCHEME, unlike MacLISP, the body of aLAMBDA
expression is not an implicitPROGN
.
At this point we are almost in a position to model the most general form of compound statement. In LISP, this is called the "PROG
feature". In addition to statement sequencing andGO TO
statements, one can return a value from aPROG
by using theRETURN
statement.
Let us first consider the simplest compound statement, which in SCHEME we callBLOCK
. Recall that(BLOCK S1 S2)
is defined to be((LAMBDA (DUMMY) S2) S1)
Notice that this not only performsS1
beforeS2
, but also returns the value ofS2
. Furthermore, we defined(BLOCK S1 S2 ... Sn)
so that it returns the value ofSn
. ThusBLOCK
may be used as a compound expression, and models a LISPPROGN
, which is aPROG
with noGO TO
statements and an implicitRETURN
of the last "statement" (really an expression).
(BLOCK x1 x2 ... xn)
(BLOCK x) → x
(BLOCK x . r) → ((LAMBDA (A B) (B)) x (LAMBDA () (BLOCK . r)))BLOCK
sequentially evaluates the subformsxi
from left to right. For example:(BLOCK (ASET' X 43) (PRINT X) (+ X 1))
returns44
after settingx
to43
and then printing it {NoteBLOCK
Exploits Applicative Order}.(LET ((v1 x1) (v2 x2) ... (vn xn)) . body)
→ ((LAMBDA (v1 v2 ... vn) (BLOCK . body)) x1 x2 ... xn)LET
provides a convenient syntax for binding several variables to corresponding quantities. It allows the forms for the quantities to appear textually adjacent to their corresponding variables. Notice that the variables are all bound simultaneously, not sequentially, and that the initialization formxi
may be evaluated in any order. For convenience,LET
also supplies aBLOCK
around the forms constituting its body.
Another difference from LISP is that SCHEME is implemented in such a way that tail-recursions execute without net growth of the interpreter stack. The effect of this is that a procedure call behaves like a GOTO
and thus procedure calls can be used to implement iterations, as in PLASMA.
Syntactic Extensions - SCHEME has a syntactic extension mechanism which provides a way to define an identifier to be a magic word, and to associate a function with that word. The function accepts the magic form as an argument, and produces a new form; this new form is then evaluated in place of the original (magic) form. This is precisely the same as the MacLISP macro facility.
(ENCLOSE fnrep envrep)
ENCLOSE
takes two s-expressions, one representing the code for a procedure, and the other representing the (lexical) environment in which is to run.ENCLOSE
returns a (closed) procedure which can be invoked.
The representation of the code is the standard s-expression description (a lambda-expression). the representation of the environment is an association list (a-list) of the traditional kind:
((var1 . value1) (var2 . value2) ...)
NIL
represents the global lexical environment. ……
TheEVALUATE
primitive described in [SCHEME] has been removed from the language. We discovered (the hard way) that the straightforward implementation ofEVALUATE
(evaluate the given expression in the current environment) destroys referential transparency. We then altered it to evaluate the expression in the top-level environment, but were still disturbed by the extent to which one is tied to a particular representation of a procedure to be executed.
This report is accompanied by a report describing standard libraries; references to this document are identified by designations such as “library section” or “library chapter”. It is also accompanied by a report containing non-normative appendices. A fourth report gives some historical background and rationales for many aspects of the language and its libraries.
At about this time, Carl Hewitt was developing his theory of actors as a model of computation. This model was object-oriented and strongly influenced by Smalltalk. Every object was a computationally active entity capable of receiving and reacting to messages. The objects were called actors, and the messages themselves were also actors. Every computational entity was an actor and message-passing was the only means of interaction. An actor could have arbitrarily many acquaintances; that is, it could “know about” other actors and send them messages or send acquaintances as (parts of) messages. Hewitt then went on to model many traditional control structures as patterns of message-passing among actors. Functional interactions were modeled with the use of continuations; one might send the actor named “factorial” a message containing two other actors: the number 5 and another actor to which to send the eventually computed value (presumably 120). While Conniver made control points into first-class data, the actors model went to the logical extreme by making everything be first-class data.
Hewitt and his students developed and implemented in Lisp a new language to make concrete the actor model of computation. This language was first called Planner-73 but the name was later changed to PLASMA (PLAnner-like System Modeled on Actors).
It was at this point that we (Sussman and Steele) put our heads together. (Steele had just become a graduate student at MIT, but he had been following this progression of language designs because he had had a part-time job at MIT Project MAC as one of the maintainers of MacLisp.) We wanted to better understand Hewitt’s actors model but were having trouble relating the actors model and its unusual terminology to familiar programming notions. We decided to construct a toy implementation of an actor language so that we could play with it. Using MacLisp as a working environment, we wrote a tiny Lisp interpreter and then added mechanisms for creating actors and sending messages. We wanted to better understand Hewitt’s actors model but were having trouble relating the actors model and its unusual terminology to familiar programming notions. We decided to construct a toy implementation of an actor language so that we could play with it. Using MacLisp as a working environment, we wrote a tiny Lisp interpreter and then added mechanisms for creating actors and sending messages.
Sussman had just been studying Algol. He suggested starting with a lexically scoped dialect of Lisp, because that seemed necessary to model the way names could refer to acquaintances in PLASMA. Lexical scoping would allow actors and functions to be created by almost identical mechanisms. Evaluating a form beginning with the wordlambda
would capture the current variable-lookup environment and create a closure; evaluating a form beginning with the wordalpha
would also capture the current environment but create an actor. Message passing could be expressed syntactically in the same way as function invocation. The difference between an actor and a function would be detected in the part of the interpreter traditionally known asapply
. A function would return a value, but an actor would never return; instead, it would typically invoke a continuation, another actor that it knew about. Our interpreter also provided the necessary primitives for implementing the internal behavior of primitive actors, such as an addition operator that could accept two numbers and a continuation actor.
This led us to three important ideas:
• First, we realized that all the patterns of control structure that Hewitt had described in terms of actors could equally well be described by the λ-calculus. ……
• Second, we realized that the λ-calculus — a small, simple formalism — could serve as the core of a powerful and expressive programming language. (Lisp had adopted the λ-notation for functions but had failed to support the appropriate behavior for free variables. The original theoretical core of Lisp was recursion equations, not the λ-calculus.) ……
• Third, we realized that in our quest for the “ultimate AI language” we had come full circle. As the MIT school had struggled to find more and more powerful ways to express and manipulate control structure to support heuristic search, we had progressed from Lisp to CONVERT to Planner to Conniver to PLASMA to a simple variation of Lisp!
We were very pleased with this toy actor implementation and named it “Schemer” because we thought it might become another AI language in the tradition of Planner and Conniver. However, the ITS operating system had a 6-character limitation on file names and so the name was truncated to simply SCHEME and that name stuck.
We concluded that actors and closures were effectively the same concept. (Hewitt later agreed with this, but noted that two types of primitive actors in his theory, namely cells (which have modifiable state) and synchronizers (which enforce exclusive access), cannot be expressed as closures in a lexically scoped pure Lisp without adding equivalent primitive extensions.)
In summary, Sussman and Steele [1975] mistakenly concluded “we discovered that the 'Actors' and the lambda expressions were identical in implementation.” The actual situation is that the λ-calculus is capable of expressing some kinds of sequential and parallel control structures but, in general, not the concurrency expressed in the Actor model. On the other hand, the Actor model is capable of expressing everything in the λ-calculus and more [Hewitt 2008f].
We were actually trying to build something complicated and discovered, serendipitously, that we had accidentally designed something that met all our goals but was much simpler than we had intended.
We thought that Scheme would turn out to be the next in a long series of programming languages that had been designed at MIT over the course of 17 years. ……
In this process we learned some great lessons. The λ-calculus can be seen as a universal glue by which compound constructs can be built inductively from simpler ones and a set of primitives. …… The essence of the matter is that the λ-calculus provides a way of abstracting entities of any particular type and then combining such abstractions with other entities to make new entities of that same type, which are instances of the abstractions. …… Given the right primitives, the λ-calculus can serve as a foundation for the abstraction and instantiation of virtually any kind of complex conceptual structure.
We also were struck by the great importance of names as a means of reference. …… Naming seems to correspond to some important cognitive mechanism that is, if not innate, then at least extremely well-developed in our culture. The λ-calculus embodies a provably coherent theory of naming.
Scheme has the unhappy distinction of being the world's most unportable programming language. It is almost misleading to call Scheme a "programming language;" it would be more accurate to characterise Scheme as a family of dialects, all loosely related by the common features of lexical scope, dynamic typing, list structure, higher-order functions, proper tail-recursion, garbage collection, macros, and (some form of) s-expression based lexical syntax.
The Revised7 Report on the Algorithmic Language Scheme (“R7RS”) intentionally describes a small language, suitable for implementing on reduced hardware, or for experiments in programming language design and implementation.
However, Scheme is used for practical programming, and there a minimal language is not a help. Additional data structures and programming idioms make the programmer’s job easier (if they are well-designed!). Therefore, as part of the R7RS design process, it was agreed that a subsequent report would appear on “R7RS-Large”, a collection of Scheme libraries that are useful across a wide variety of problems.
The decision of what was to appear in R7RS-Large was left to the community, and a strategy was adopted: proposals were published as SRFIs (Scheme Requests For Implementation, http://srfi.schemers.org), and the community would then vote on which would be adopted.
As a result, R7RS-Large will be developed in several stages, depending upon the support from the community for each proposal. At each stage, several SRFIs are frozen, and implementers and users may then rely upon those libraries being in the final product. (In a few cases, a library might be revisited, with a subsequent stage adopting a completely backward-compatible new version of that library; this has already happened with generators.)
When this process has completed, the resulting interim reports will be collated and reorganized into a final document.
Scheme is a statically scoped and properly tail-recursive dialect of the Lisp programming language invented by Guy Lewis Steele Jr. and Gerald Jay Sussman. It was designed to have an exceptionally clear and simple semantics and few different ways to form expressions. A wide variety of programming paradigms, including imperative, functional, and message passing styles, find convenient expression in Scheme.
The variables in a lambda expression are dummy or bound variables because systematically changing them does not alter the meaning of the expression. Thusλ[[u;v];v2+u]
means the same thing asλ[[x;y];y2+x]
.
We shall sometimes use expressions in which a variable is not bound by a lambda. For example, in the function of two variablesλ[[x;y];xn+yn]
the variablen
is not bound. This is called a free variable. It may be regarded as a parameter. Unlessn
has been given a value before trying to compute with this function, the value of the function must be undefined. ……
When the compiler is called upon to compile a function, it looks for anEXPR
orFEXPR
on the property list of the function name. The compiler then translates this S-expression into an S-expression that represents a subroutine in the LISP Assembly Language (LAP). LAP then proceeds to assemble this program into binary program space. Thus anEXPR
, or anFEXPR
, has been changed to aSUBR
or anFSUBR
, respectively. ……
Free variables in compiled functions must be declared before the function is compiled. ……
A variable is bound in a particular function when it occurs in a list of bound variables following the wordLAMBDA
orPROG
. Any variable that is not bound is free. ……
When a variable is used free, it must have been bound by a higher level function. If a program is being run interpretively, and a free variable is used without having bee bound on a higher level, error diagnostic *A 8* will occur.
If the program is being run complied, the diagnostic may not occur, and the variable may have value NIL.
There are three types of variables in compiled functions: ordinary variables,SPECIAL
variables, andCOMMON
variables.SPECIAL
andCOMMON
variables must be declared before compiling. Any variable that is not declared will be considered an ordinary variable.
When functions are translated into subroutines, the concept of a variable is translated into a location where an argument is stored. If the variable is an ordinary one, then a storage location for it is set up on the push-down list. Other functions cannot find this private cell, making it impossible to use it as a free variable.SPECIAL
variables have the indicatorSPECIAL
on their property lists Following the indicator there is a pointer to a fixed cell. When this variable is bound, the old value is saved on the push-down list, and the current value is stored in theSPECLAL
cell. When it is no longer bound, the old value must be restored. When a function uses this variable free, then the quantity in theSPECIAL
cell is picked up.SPECIAL
variables are declared by using the pseudo-functionspecial[a]
, wherea
is a list of variable names. ……SPECIAL
variables are inexpensive and will allow free communication among compiled functions. They do not increase run time significantly.SPECIAL
variables cannot be communicated between the interpreter and compiled functions.COMMON
variables have the flagCOMMON
on their property lists; however, this is only used to inform the compiler that they areCOMMON
, and is not needed at run time.COMMON
variables are bound on an a-list by the compiled functions. When they are to be evaluated,eval
is given this a-list. This happens at run time.
The use ofCOMMON
variables will slow down any compiled function using them. However, they do provide complete communication between interpreted and compiled functions.COMMON
variables are declared bycommon[a]
, wherea
is a list of variable names. ……
LISP is designed in such a way that all functions for which the possibility of recursion can exist are in fact recursive. This requires that all temporary stored results related to the computation that is in progress be set aside when a piece of coding is to be used recursively, and that they be later restored. This is done automatically and need not be programmed explicitly.
All saving of temporary results in LISP is performed on a linear block of storage called the push-down list. Each set of stored data that is moved onto the push-down list is in a block labeled with its size and the name of the subroutine from which it came. Since it is in the nature of recursion that the first block to be saved is always the last block to be restored, it is possible to keep the push-down list compact.
There are several ways in which to maintain the values of free variables (and, thus the computational environment) in order to handle cases like the one above. All of the techniques used involve some increase in time. One approach makes it easy to access the value of a free variable is local. This approach is favored in recent implementations of LISP. We shall call it the "shallow access" approach.
Another approach is to make it relatively easy to enter and leave a block in which a free variable is bound, but relatively expensive to access a free variable. This approach is used in the classical "alist" implementation of LISP. Several Algol systems have opted for a similar approach. We shall call this the "deep access" approach. Both of these approaches allow one to modify the value of the variable as well as access it. The access time is approximately the same as modification time in both approaches.
Let us consider the "shallow access" approach first. By "shallow access" we mean that the value of a free variable can be obtained in a single fetch from memory. Since the current value may be stored in a difficult-to-determine location up in the stack, a special cell for its current value is used. In many recent LISP implementations this special values cell is stored as a property of the atom structure, and is unique to the free variable. In order to maintain the proper value in the special cell, extra work must be done every time a function or a block is entered in which the variable is an argument or is local. On entering such a function or block one usually saves the old value which is in the special cell on the stack alone with sufficient information to allow one to know from where the stack value came. On leaving such a function or block one must remember to store the value saved in the stack in the special cell.
The "deep access" approach forces one to search upward in the stack for the most recent value of the free variable. Such a search may be quite deep and slow if the free variable was bond vary far up in the stack. However, this approach has the advantage of avoiding the need for an extra special value cell. In addition, we shall see later that this approach sometimes allows one to use free variables with greater ease and flexibility than the "shallow access" approach. ……
Certain LISP systems use "deep access" in interpreted functions and "shallow access" in compiled functions. if free variables in compiled functions are declared to beCOMMON
, their values will be stored on the "alist". In this manner one can solve the environment problem. The cost is a very slow interpreter and "deep access" to free variables.
To use functions as arguments, one needs a notation for functions, and it seemed natural to use the λ-notation of Church (1941). I didn’t understand the rest of his book, so I wasn’t tempted to try to implement his more general mechanism for defining functions. Church used higher order functionals instead of using conditional expressions. Conditional expressions are much more readily implemented on computers.
LISP was not based on the lambda calculus, despite using the word “LAMBDA” to denote functions. At the time he invented LISP, McCarthy was aware of (Church 1941) but had not studied it. The theoretical model behind LISP was Kleene’s theory of first order recursive functions. (McCarthy made these statements, or very similar ones, in a contribution from the floor at the 1982 ACM symposium on LISP and functional programming in Pittsburgh. No written version of this exists, as far as know.)
Scheme has the unhappy distinction of being the world's most unportable programming language. It is almost misleading to call Scheme a "programming language;" it would be more accurate to characterise Scheme as a family of dialects, all loosely related by the common features of lexical scope, dynamic typing, list structure, higher-order functions, proper tail-recursion, garbage collection, macros, and (some form of) s-expression based lexical syntax.
To use functions as arguments, one needs a notation for functions, and it seemed natural to use the λ-notation of Church (1941). I didn’t understand the rest of his book, so I wasn’t tempted to try to implement his more general mechanism for defining functions. Church used higher order functionals instead of using conditional expressions. Conditional expressions are much more readily implemented on computers.
LISP was not based on the lambda calculus, despite using the word “LAMBDA” to denote functions. At the time he invented LISP, McCarthy was aware of (Church 1941) but had not studied it. The theoretical model behind LISP was Kleene’s theory of first order recursive functions. (McCarthy made these statements, or very similar ones, in a contribution from the floor at the 1982 ACM symposium on LISP and functional programming in Pittsburgh. No written version of this exists, as far as know.)
At about this time, Carl Hewitt was developing his theory of actors as a model of computation. This model was object-oriented and strongly influenced by Smalltalk. Every object was a computationally active entity capable of receiving and reacting to messages. The objects were called actors, and the messages themselves were also actors. Every computational entity was an actor and message-passing was the only means of interaction. An actor could have arbitrarily many acquaintances; that is, it could “know about” other actors and send them messages or send acquaintances as (parts of) messages. Hewitt then went on to model many traditional control structures as patterns of message-passing among actors. Functional interactions were modeled with the use of continuations; one might send the actor named “factorial” a message containing two other actors: the number 5 and another actor to which to send the eventually computed value (presumably 120). While Conniver made control points into first-class data, the actors model went to the logical extreme by making everything be first-class data.
(%A M%)
to indicate sending the message M to the actor A. We shall use [s1 s2 … sn]
to denote the finite squence s1, s2, … sn. ……=
. ……(=> pattern body)
is an abbreviation for (receive(message pattern) body)
where receive is a more general actor that is capable of bindinq elements of the action in addition to the message.(%(=> pattern body) the-message%)
, i.e. sending(=> pattern body) the-message
,NOT-APPLICABLE
to the-message. If the-message matches pattern, the body is evaluated.(send T
(message M
[#continuation C]))
The transmission (%T M%)
is an abbreviation for the above where C is defaulted to be the caller. If target T is the following:
(receive
(message the-body
[#continuation =the-continuation])
the-body)
then the the-body is evaluated in an environment where the-message is bound to M and the-continuation is bound to C.
……
Many actors who are executing in parallel can share the same continuation. They can all send a message [“return”]
to the same continuation. ”
Irene Greif, Carl Hewitt. Actor semantics of PLANNER-73. 1975 [2022-11-15]. (原始内容存档于2022-11-15).
Hewitt and his students developed and implemented in Lisp a new language to make concrete the actor model of computation. This language was first called Planner-73 but the name was later changed to PLASMA (PLAnner-like System Modeled on Actors).
It was at this point that we (Sussman and Steele) put our heads together. (Steele had just become a graduate student at MIT, but he had been following this progression of language designs because he had had a part-time job at MIT Project MAC as one of the maintainers of MacLisp.) We wanted to better understand Hewitt’s actors model but were having trouble relating the actors model and its unusual terminology to familiar programming notions. We decided to construct a toy implementation of an actor language so that we could play with it. Using MacLisp as a working environment, we wrote a tiny Lisp interpreter and then added mechanisms for creating actors and sending messages. We wanted to better understand Hewitt’s actors model but were having trouble relating the actors model and its unusual terminology to familiar programming notions. We decided to construct a toy implementation of an actor language so that we could play with it. Using MacLisp as a working environment, we wrote a tiny Lisp interpreter and then added mechanisms for creating actors and sending messages.
Also we represent the value of a λ-expression by a bundle of information called a "closure," comprising the λ-expression and the environment relative to which it was evaluated. We must therefore arrange that such a bundle is correctly interpreted whenever it has to be applied to some argument. More precisely:
a closure has
an environment part which is a list whose two items are:
⑴ an environment
⑵ an identifier or list of identifiers,
and a control part which consists of a list whose sole item is an AE.
The value relative toE
of a λ-expressionX
is represented by the closure denoted by
constructclosure((E, bvX), unitlist(bodyX))
The use of free variables is a very powerful computational idea. In many situations it is possible to avoid using free variables by supplying a function with additional arguments. This can be quite cumbersome. ……
When a function uses free variables or when it calls functions which use them, then the value of this function is not completely determined by its arguments, since the value might depend on the values of the free variables. Thus in order to completely specify a computation, one has to specify a function and its arguments in addition to an environment in which the values of the free variables can be determined. ……
An important point which must be realized about functional arguments (abbreviated FUNARGs) is that two different environments are involved in such cases. The first environment is the one which is in effect when then FUNARG is bound as an argument. We shall call this the binding environment. The second environment is the one that is in effect when the FUNARG is activated as a function. We shall call this the activation environment. ……
Consider now what it would require for the system to restore the binding environment for functional arguments. It would require knowing where in the stack or "alist" the binding environment exists through some pointer to it. Supplying such pointer is a functionFUNCTION
in LISP. That is when one transmits a functional argumentf
which is to be evaluated in its binding environment, then one usesFUNCTION(f)
.FUNCTION
will prevent its argumentf
from being evaluated, just asQUOTE
would. The result ofFUNCTION
will be a structure which not only contains a reference to the functionf
, but also contains a pointer to the binding environment. Thus at the FUNARG's activation time we will be able to use the current place in the stack up to the point where the binding environment exists, thus skipping over any values in the activation environment which differ from those in the binding environment. ……
The points we have made so far are:
1) Free variables in function definitions require that one must have an environment in order to be able to evaluate a function.
2) When one uses functional arguments, the the question arises as to which environment one chooses in order to evaluate the function, the binding environment or the activation environment.
3) When one returns functions (which require the values of free variables for their evaluation) then one must, in general, save the binding environment and thus create a stack which is, in fact, a tree. ……
LISP allows the user to choose between the binding environment and the activation environment by allowing a function to be bound withFUNCTION
or withQUOTE
.QUOTE
will force the activation environment to be used in evaluating the function. A useful metaphor for the difference betweenFUNCTION
andQUOTE
in LISP is to think ofQUOTE
as a porous or an open covering of the function since free variables escape to the current environment.FUNCTION
acts as a closed or nonporous covering (hence the term "closure" used by Landin). Thus we talk of "open" Lambda expressions (functions in LISP are usually Lambda expressions) and "closed" Lambda expressions. ……
My interest in the environment problem began while Landin, who had a deep understanding of the problem, visited MIT during 1966-67. I then realized the correspondence between the FUNARG lists which are the results of the evaluation of "closed" Lambda expressions in LISP and ISWIM's Lambda Closures.
Sussman had just been studying Algol. He suggested starting with a lexically scoped dialect of Lisp, because that seemed necessary to model the way names could refer to acquaintances in PLASMA. Lexical scoping would allow actors and functions to be created by almost identical mechanisms. Evaluating a form beginning with the wordlambda
would capture the current variable-lookup environment and create a closure; evaluating a form beginning with the wordalpha
would also capture the current environment but create an actor. Message passing could be expressed syntactically in the same way as function invocation. The difference between an actor and a function would be detected in the part of the interpreter traditionally known asapply
. A function would return a value, but an actor would never return; instead, it would typically invoke a continuation, another actor that it knew about. Our interpreter also provided the necessary primitives for implementing the internal behavior of primitive actors, such as an addition operator that could accept two numbers and a continuation actor.
This led us to three important ideas:
• First, we realized that all the patterns of control structure that Hewitt had described in terms of actors could equally well be described by the λ-calculus. ……
• Second, we realized that the λ-calculus — a small, simple formalism — could serve as the core of a powerful and expressive programming language. (Lisp had adopted the λ-notation for functions but had failed to support the appropriate behavior for free variables. The original theoretical core of Lisp was recursion equations, not the λ-calculus.) ……
• Third, we realized that in our quest for the “ultimate AI language” we had come full circle. As the MIT school had struggled to find more and more powerful ways to express and manipulate control structure to support heuristic search, we had progressed from Lisp to CONVERT to Planner to Conniver to PLASMA to a simple variation of Lisp!
Inspired by ACTORS [Greif and Hewitt][Smith and Hewitt], we have implemented an interpreter for a LISP-like language, SCHEME, based on the lambda calculus [Church], but extended for side effects, multiprocessing, and process synchronization. The purpose of this implementation is tutorial. We wish to:
⑴ alleviate the confusion caused by Micro-PLANNER, CONNIVER, etc. by clarifying the embedding of non-recursive control structures in a recursive host language like LISP.
⑵ explain how to use these control structures, independent of such issues as pattern matching and data base manipulation.
⑶ have a simple concrete experimental domain for certain issues of programming semantics and style.
We were very pleased with this toy actor implementation and named it “Schemer” because we thought it might become another AI language in the tradition of Planner and Conniver. However, the ITS operating system had a 6-character limitation on file names and so the name was truncated to simply SCHEME and that name stuck.
We concluded that actors and closures were effectively the same concept. (Hewitt later agreed with this, but noted that two types of primitive actors in his theory, namely cells (which have modifiable state) and synchronizers (which enforce exclusive access), cannot be expressed as closures in a lexically scoped pure Lisp without adding equivalent primitive extensions.)
In summary, Sussman and Steele [1975] mistakenly concluded “we discovered that the 'Actors' and the lambda expressions were identical in implementation.” The actual situation is that the λ-calculus is capable of expressing some kinds of sequential and parallel control structures but, in general, not the concurrency expressed in the Actor model. On the other hand, the Actor model is capable of expressing everything in the λ-calculus and more [Hewitt 2008f].
SCHEME is a dialect of LISP. It is an expression-oriented, applicative order, interpreter-based language which allows one to manipulate programs as data. It differs from most current dialects of LISP in that it closes all lambda-expressions in the environment of their definition or declaration, rather than in the execution environment. This has the consequence that variables are normally lexically scoped, as in ALGOL. However, in contrast with ALGOL, SCHEME treats procedures as a first-class data type. They can be the values of variables, the returned values of procedures, and components of data structures. Another difference from LISP is that SCHEME is implemented in such a way that tail-recursions execute without net growth of the interpreter stack. The effect of this is that a procedure call behaves like a GOTO
and thus procedure calls can be used to implement iterations, as in PLASMA.
We were actually trying to build something complicated and discovered, serendipitously, that we had accidentally designed something that met all our goals but was much simpler than we had intended.
We thought that Scheme would turn out to be the next in a long series of programming languages that had been designed at MIT over the course of 17 years. ……
In this process we learned some great lessons. The λ-calculus can be seen as a universal glue by which compound constructs can be built inductively from simpler ones and a set of primitives. …… The essence of the matter is that the λ-calculus provides a way of abstracting entities of any particular type and then combining such abstractions with other entities to make new entities of that same type, which are instances of the abstractions. …… Given the right primitives, the λ-calculus can serve as a foundation for the abstraction and instantiation of virtually any kind of complex conceptual structure.
We also were struck by the great importance of names as a means of reference. …… Naming seems to correspond to some important cognitive mechanism that is, if not innate, then at least extremely well-developed in our culture. The λ-calculus embodies a provably coherent theory of naming.
Scheme is a statically scoped and properly tail-recursive dialect of the Lisp programming language invented by Guy Lewis Steele Jr. and Gerald Jay Sussman. It was designed to have an exceptionally clear and simple semantics and few different ways to form expressions. A wide variety of programming paradigms, including imperative, functional, and message passing styles, find convenient expression in Scheme.
The Revised7 Report on the Algorithmic Language Scheme (“R7RS”) intentionally describes a small language, suitable for implementing on reduced hardware, or for experiments in programming language design and implementation.
However, Scheme is used for practical programming, and there a minimal language is not a help. Additional data structures and programming idioms make the programmer’s job easier (if they are well-designed!). Therefore, as part of the R7RS design process, it was agreed that a subsequent report would appear on “R7RS-Large”, a collection of Scheme libraries that are useful across a wide variety of problems.
The decision of what was to appear in R7RS-Large was left to the community, and a strategy was adopted: proposals were published as SRFIs (Scheme Requests For Implementation, http://srfi.schemers.org), and the community would then vote on which would be adopted.
As a result, R7RS-Large will be developed in several stages, depending upon the support from the community for each proposal. At each stage, several SRFIs are frozen, and implementers and users may then rely upon those libraries being in the final product. (In a few cases, a library might be revisited, with a subsequent stage adopting a completely backward-compatible new version of that library; this has already happened with generators.)
When this process has completed, the resulting interim reports will be collated and reorganized into a final document.
Data and procedures and the values they amass, Higher-order functions to combine and mix and match, Objects with their local state, the message they pass, A property, a package, the control of point for a catch - In the Lambda Order they are all first-class. One thing to name them all, one things to define them, one thing to place them in environments and bind them, in the Lambda Order they are all first-class. ……
Scheme shares with Common Lisp the goal of a core language common to several implementations. Scheme differs from Common Lisp in its emphasis upon simplicity and function over compatibility with older dialects of Lisp.
In the interpreter “variables” are implemented as atomic symbols which possess “shallow-bound” value cells. The continual manipulation of value cells would decrease the efficiency of compiled code, so the compiler defines two types of variables: “special variables” and “local variables.” Special variables are identical to variables in the interpreter.
Local variables are more like the variables in commonly-used algebraic programming languages such as Algol or PL/I. A local variable no longer has an associated atomic symbol; thus it can only be referred to from the text of the same function that bound it. The compiler creates local variables forPROG
variables,DO
variables, andLAMBDA
variables, unless directed otherwise. The compiled code stores local variables in machine registers or in locations within a stack.
The principal difference between local variables and special variables is in the way a binding of a variable is compiled. (A binding has to be compiled when aPROG
,DO
, orLAMBDA
expression is compiled, and for the entry to a function which hasLAMBDA
variables to be bound to its arguments.) If the variable to be bound has been declared to be special, the binding is compiled as code to imitate the way the interpreter binds variables: the value of the atomic symbol is saved and a new value is stored into its value cell. If the variable to be bound has not been declared special, the binding is compiled as the declaration of a new local variable. Code is generated to store the value to which the variable is to be bound into the register or stack-location assigned to the new local variable. This runs considerably faster than a special binding.
Although a special variable is associated with an atomic symbol which has the name of the variable, the name of a local variable appears only in the input file - in compiled code there is no connection between local variables and atomic symbols. Because this is so, a local variable in one function may not be used as a “free variable” in another function since there is no way for the location of the variable to be communicated between the two functions.
When the usage of a variable in a program to be compiled does not conform to these rules, i.e. it is somewhere used as a “free variable,” the variable must be declared special. There are two common cases in which this occurs. One is where a “global” variable is being used, i.e. a variable which isSETQ
'ed by many functions but is never bound. The other is where two functions cooperate, one binding a variable and then calling the other one which uses that variable as a free variable.
The variables in a lambda expression are dummy or bound variables because systematically changing them does not alter the meaning of the expression. Thusλ[[u;v];v2+u]
means the same thing asλ[[x;y];y2+x]
.
We shall sometimes use expressions in which a variable is not bound by a lambda. For example, in the function of two variablesλ[[x;y];xn+yn]
the variablen
is not bound. This is called a free variable. It may be regarded as a parameter. Unlessn
has been given a value before trying to compute with this function, the value of the function must be undefined. ……
When the compiler is called upon to compile a function, it looks for anEXPR
orFEXPR
on the property list of the function name. The compiler then translates this S-expression into an S-expression that represents a subroutine in the LISP Assembly Language (LAP). LAP then proceeds to assemble this program into binary program space. Thus anEXPR
, or anFEXPR
, has been changed to aSUBR
or anFSUBR
, respectively. ……
Free variables in compiled functions must be declared before the function is compiled. ……
A variable is bound in a particular function when it occurs in a list of bound variables following the wordLAMBDA
orPROG
. Any variable that is not bound is free. ……
When a variable is used free, it must have been bound by a higher level function. If a program is being run interpretively, and a free variable is used without having bee bound on a higher level, error diagnostic *A 8* will occur.
If the program is being run complied, the diagnostic may not occur, and the variable may have value NIL.
There are three types of variables in compiled functions: ordinary variables,SPECIAL
variables, andCOMMON
variables.SPECIAL
andCOMMON
variables must be declared before compiling. Any variable that is not declared will be considered an ordinary variable.
When functions are translated into subroutines, the concept of a variable is translated into a location where an argument is stored. If the variable is an ordinary one, then a storage location for it is set up on the push-down list. Other functions cannot find this private cell, making it impossible to use it as a free variable.SPECIAL
variables have the indicatorSPECIAL
on their property lists Following the indicator there is a pointer to a fixed cell. When this variable is bound, the old value is saved on the push-down list, and the current value is stored in theSPECLAL
cell. When it is no longer bound, the old value must be restored. When a function uses this variable free, then the quantity in theSPECIAL
cell is picked up.SPECIAL
variables are declared by using the pseudo-functionspecial[a]
, wherea
is a list of variable names. ……SPECIAL
variables are inexpensive and will allow free communication among compiled functions. They do not increase run time significantly.SPECIAL
variables cannot be communicated between the interpreter and compiled functions.COMMON
variables have the flagCOMMON
on their property lists; however, this is only used to inform the compiler that they areCOMMON
, and is not needed at run time.COMMON
variables are bound on an a-list by the compiled functions. When they are to be evaluated,eval
is given this a-list. This happens at run time.
The use ofCOMMON
variables will slow down any compiled function using them. However, they do provide complete communication between interpreted and compiled functions.COMMON
variables are declared bycommon[a]
, wherea
is a list of variable names. ……
LISP is designed in such a way that all functions for which the possibility of recursion can exist are in fact recursive. This requires that all temporary stored results related to the computation that is in progress be set aside when a piece of coding is to be used recursively, and that they be later restored. This is done automatically and need not be programmed explicitly.
All saving of temporary results in LISP is performed on a linear block of storage called the push-down list. Each set of stored data that is moved onto the push-down list is in a block labeled with its size and the name of the subroutine from which it came. Since it is in the nature of recursion that the first block to be saved is always the last block to be restored, it is possible to keep the push-down list compact.
There are several ways in which to maintain the values of free variables (and, thus the computational environment) in order to handle cases like the one above. All of the techniques used involve some increase in time. One approach makes it easy to access the value of a free variable is local. This approach is favored in recent implementations of LISP. We shall call it the "shallow access" approach.
Another approach is to make it relatively easy to enter and leave a block in which a free variable is bound, but relatively expensive to access a free variable. This approach is used in the classical "alist" implementation of LISP. Several Algol systems have opted for a similar approach. We shall call this the "deep access" approach. Both of these approaches allow one to modify the value of the variable as well as access it. The access time is approximately the same as modification time in both approaches.
Let us consider the "shallow access" approach first. By "shallow access" we mean that the value of a free variable can be obtained in a single fetch from memory. Since the current value may be stored in a difficult-to-determine location up in the stack, a special cell for its current value is used. In many recent LISP implementations this special values cell is stored as a property of the atom structure, and is unique to the free variable. In order to maintain the proper value in the special cell, extra work must be done every time a function or a block is entered in which the variable is an argument or is local. On entering such a function or block one usually saves the old value which is in the special cell on the stack alone with sufficient information to allow one to know from where the stack value came. On leaving such a function or block one must remember to store the value saved in the stack in the special cell.
The "deep access" approach forces one to search upward in the stack for the most recent value of the free variable. Such a search may be quite deep and slow if the free variable was bond vary far up in the stack. However, this approach has the advantage of avoiding the need for an extra special value cell. In addition, we shall see later that this approach sometimes allows one to use free variables with greater ease and flexibility than the "shallow access" approach. ……
Certain LISP systems use "deep access" in interpreted functions and "shallow access" in compiled functions. if free variables in compiled functions are declared to beCOMMON
, their values will be stored on the "alist". In this manner one can solve the environment problem. The cost is a very slow interpreter and "deep access" to free variables.
DEFINE
- This is analogous to the MacLISPDEFUN
primitive (but note that theLAMBDA
must appear explicitly!). It is used for defining a function in the "global environment" permanently, as opposed toLABELS
(see below), which is used for temporary definitions in a local environment.DEFINE
takes a name and a lambda expression; it closes the lambda expression in the global environment and stores the closure in the LISP value cell of the name (which is a LISP atom). ……LABELS
- We have decided not to use the traditionalLABEL
primitive in this interpreter because it is difficult to define several mutually recursive functions using onlyLABEL
. The solution, which Hewitt [Smith and Hewitt] also uses, is to adopt an ALGOL-esque block syntax:
(LABELS <function definition list> <expression>)
This has the effect of evaluating the expression in an environment where all the functions are defined as specified by the definitions list. Furthermore, the functions are themselves closed in that environment, and not in the outer environment; this allows the functions to call themselves and each other recursively.
Atoms which are not atomic symbols (identifiers) evaluate to themselves. Typical examples of such atoms are numbers, arrays, and strings (character arrays). Symbols are treated as identifiers or variables. They may be lexically bound by lambda-expressions. There is a global environment containing values initially have as their values primitive operations such as, for example,CAR
,CONS
, andPLUS
. SCHEME differs from most LISP systems in that the atomCAR
is not itself an operation (in the sense of being an invocable object, e.g. a valid first argument toAPPLY
), but only has one as a value when considered as an identifier.
Another difference from LISP is that SCHEME is implemented in such a way that tail-recursions execute without net growth of the interpreter stack. The effect of this is that a procedure call behaves like a GOTO
and thus procedure calls can be used to implement iterations, as in PLASMA.
Syntactic Extensions - SCHEME has a syntactic extension mechanism which provides a way to define an identifier to be a magic word, and to associate a function with that word. The function accepts the magic form as an argument, and produces a new form; this new form is then evaluated in place of the original (magic) form. This is precisely the same as the MacLISP macro facility.
In most current Lisp systems the expander's task is confined to the process of finding syntactic extensions and replacing them by their expansions. This implies, in particular, the each macro function is responsible for the integrity of the program. For Lisp systems (and other languages with similar macro facilities) this means specifically that variable binding must not be corrupted. This, however, is not as simple a task as it sounds. ……
The real danger of these erroneous macros is that they are treacherous. They work in all cases but one: when the user - or some other macro writer - inadvertently picks the wrong identifier name.
Various techniques have been proposed to circumvent this capturing problem, but they rely on the individual macro writer. if even one of many macro writers is negligent, the macro system becomes unsafe. We claim that the task of safely renaming macro-generated identifier is mechanical. It is essentially an α-conversion, which is knowledgeable about the origin of the identifiers. For these reasons we propose a change to the navïe macro expansion algorithm which automatically maintains hygienic conditions during expansion time.
(ENCLOSE fnrep envrep)
ENCLOSE
takes two s-expressions, one representing the code for a procedure, and the other representing the (lexical) environment in which is to run.ENCLOSE
returns a (closed) procedure which can be invoked.
The representation of the code is the standard s-expression description (a lambda-expression). the representation of the environment is an association list (a-list) of the traditional kind:
((var1 . value1) (var2 . value2) ...)
NIL
represents the global lexical environment. ……
TheEVALUATE
primitive described in [SCHEME] has been removed from the language. We discovered (the hard way) that the straightforward implementation ofEVALUATE
(evaluate the given expression in the current environment) destroys referential transparency. We then altered it to evaluate the expression in the top-level environment, but were still disturbed by the extent to which one is tied to a particular representation of a procedure to be executed.
This report is accompanied by a report describing standard libraries; references to this document are identified by designations such as “library section” or “library chapter”. It is also accompanied by a report containing non-normative appendices. A fourth report gives some historical background and rationales for many aspects of the language and its libraries.
To use functions as arguments, one needs a notation for functions, and it seemed natural to use the λ-notation of Church (1941). I didn’t understand the rest of his book, so I wasn’t tempted to try to implement his more general mechanism for defining functions. Church used higher order functionals instead of using conditional expressions. Conditional expressions are much more readily implemented on computers.
LISP was not based on the lambda calculus, despite using the word “LAMBDA” to denote functions. At the time he invented LISP, McCarthy was aware of (Church 1941) but had not studied it. The theoretical model behind LISP was Kleene’s theory of first order recursive functions. (McCarthy made these statements, or very similar ones, in a contribution from the floor at the 1982 ACM symposium on LISP and functional programming in Pittsburgh. No written version of this exists, as far as know.)
The use of free variables is a very powerful computational idea. In many situations it is possible to avoid using free variables by supplying a function with additional arguments. This can be quite cumbersome. ……
When a function uses free variables or when it calls functions which use them, then the value of this function is not completely determined by its arguments, since the value might depend on the values of the free variables. Thus in order to completely specify a computation, one has to specify a function and its arguments in addition to an environment in which the values of the free variables can be determined. ……
An important point which must be realized about functional arguments (abbreviated FUNARGs) is that two different environments are involved in such cases. The first environment is the one which is in effect when then FUNARG is bound as an argument. We shall call this the binding environment. The second environment is the one that is in effect when the FUNARG is activated as a function. We shall call this the activation environment. ……
Consider now what it would require for the system to restore the binding environment for functional arguments. It would require knowing where in the stack or "alist" the binding environment exists through some pointer to it. Supplying such pointer is a functionFUNCTION
in LISP. That is when one transmits a functional argumentf
which is to be evaluated in its binding environment, then one usesFUNCTION(f)
.FUNCTION
will prevent its argumentf
from being evaluated, just asQUOTE
would. The result ofFUNCTION
will be a structure which not only contains a reference to the functionf
, but also contains a pointer to the binding environment. Thus at the FUNARG's activation time we will be able to use the current place in the stack up to the point where the binding environment exists, thus skipping over any values in the activation environment which differ from those in the binding environment. ……
The points we have made so far are:
1) Free variables in function definitions require that one must have an environment in order to be able to evaluate a function.
2) When one uses functional arguments, the the question arises as to which environment one chooses in order to evaluate the function, the binding environment or the activation environment.
3) When one returns functions (which require the values of free variables for their evaluation) then one must, in general, save the binding environment and thus create a stack which is, in fact, a tree. ……
LISP allows the user to choose between the binding environment and the activation environment by allowing a function to be bound withFUNCTION
or withQUOTE
.QUOTE
will force the activation environment to be used in evaluating the function. A useful metaphor for the difference betweenFUNCTION
andQUOTE
in LISP is to think ofQUOTE
as a porous or an open covering of the function since free variables escape to the current environment.FUNCTION
acts as a closed or nonporous covering (hence the term "closure" used by Landin). Thus we talk of "open" Lambda expressions (functions in LISP are usually Lambda expressions) and "closed" Lambda expressions. ……
My interest in the environment problem began while Landin, who had a deep understanding of the problem, visited MIT during 1966-67. I then realized the correspondence between the FUNARG lists which are the results of the evaluation of "closed" Lambda expressions in LISP and ISWIM's Lambda Closures.
Inspired by ACTORS [Greif and Hewitt][Smith and Hewitt], we have implemented an interpreter for a LISP-like language, SCHEME, based on the lambda calculus [Church], but extended for side effects, multiprocessing, and process synchronization. The purpose of this implementation is tutorial. We wish to:
⑴ alleviate the confusion caused by Micro-PLANNER, CONNIVER, etc. by clarifying the embedding of non-recursive control structures in a recursive host language like LISP.
⑵ explain how to use these control structures, independent of such issues as pattern matching and data base manipulation.
⑶ have a simple concrete experimental domain for certain issues of programming semantics and style.
We concluded that actors and closures were effectively the same concept. (Hewitt later agreed with this, but noted that two types of primitive actors in his theory, namely cells (which have modifiable state) and synchronizers (which enforce exclusive access), cannot be expressed as closures in a lexically scoped pure Lisp without adding equivalent primitive extensions.)
In summary, Sussman and Steele [1975] mistakenly concluded “we discovered that the 'Actors' and the lambda expressions were identical in implementation.” The actual situation is that the λ-calculus is capable of expressing some kinds of sequential and parallel control structures but, in general, not the concurrency expressed in the Actor model. On the other hand, the Actor model is capable of expressing everything in the λ-calculus and more [Hewitt 2008f].
Scheme is a statically scoped and properly tail-recursive dialect of the Lisp programming language invented by Guy Lewis Steele Jr. and Gerald Jay Sussman. It was designed to have an exceptionally clear and simple semantics and few different ways to form expressions. A wide variety of programming paradigms, including imperative, functional, and message passing styles, find convenient expression in Scheme.
This report is accompanied by a report describing standard libraries; references to this document are identified by designations such as “library section” or “library chapter”. It is also accompanied by a report containing non-normative appendices. A fourth report gives some historical background and rationales for many aspects of the language and its libraries.
Data and procedures and the values they amass, Higher-order functions to combine and mix and match, Objects with their local state, the message they pass, A property, a package, the control of point for a catch - In the Lambda Order they are all first-class. One thing to name them all, one things to define them, one thing to place them in environments and bind them, in the Lambda Order they are all first-class. ……
Scheme shares with Common Lisp the goal of a core language common to several implementations. Scheme differs from Common Lisp in its emphasis upon simplicity and function over compatibility with older dialects of Lisp.
System-Provided Extensions - Some standard syntactic extension are provided by convenience in ordinary programming. They are distinguished from other magic words in that they are semantically defined in terms of others rather then being primitive {Note FEXPRs Are Okay by Us}. For expository purposes they are described here pattern-matching/production-rules kind of language. takes advantage of the definition of list notation:(A B C) = (A . (B . (C . NIL)))
. Thus the pattern(x . r)
matches(A B C)
, withx = A
andr = (B C)
. the ordering of the "productions" is significant; the first one which matches is to be used.
In the interpreter “variables” are implemented as atomic symbols which possess “shallow-bound” value cells. The continual manipulation of value cells would decrease the efficiency of compiled code, so the compiler defines two types of variables: “special variables” and “local variables.” Special variables are identical to variables in the interpreter.
Local variables are more like the variables in commonly-used algebraic programming languages such as Algol or PL/I. A local variable no longer has an associated atomic symbol; thus it can only be referred to from the text of the same function that bound it. The compiler creates local variables forPROG
variables,DO
variables, andLAMBDA
variables, unless directed otherwise. The compiled code stores local variables in machine registers or in locations within a stack.
The principal difference between local variables and special variables is in the way a binding of a variable is compiled. (A binding has to be compiled when aPROG
,DO
, orLAMBDA
expression is compiled, and for the entry to a function which hasLAMBDA
variables to be bound to its arguments.) If the variable to be bound has been declared to be special, the binding is compiled as code to imitate the way the interpreter binds variables: the value of the atomic symbol is saved and a new value is stored into its value cell. If the variable to be bound has not been declared special, the binding is compiled as the declaration of a new local variable. Code is generated to store the value to which the variable is to be bound into the register or stack-location assigned to the new local variable. This runs considerably faster than a special binding.
Although a special variable is associated with an atomic symbol which has the name of the variable, the name of a local variable appears only in the input file - in compiled code there is no connection between local variables and atomic symbols. Because this is so, a local variable in one function may not be used as a “free variable” in another function since there is no way for the location of the variable to be communicated between the two functions.
When the usage of a variable in a program to be compiled does not conform to these rules, i.e. it is somewhere used as a “free variable,” the variable must be declared special. There are two common cases in which this occurs. One is where a “global” variable is being used, i.e. a variable which isSETQ
'ed by many functions but is never bound. The other is where two functions cooperate, one binding a variable and then calling the other one which uses that variable as a free variable.
There are several important consequences of closing every lambda expression in the environment from which it is passed (i.e., in its "lexical" or "static" environment).
First, the axioms of lambda calculus are automatically preserved. Thus, referential transparency is enforced. This in turn implies that there are no "fluid" variable bindings (as there are in standard stack implementations of LISP such as MacLISP).
Second, the upward funarg problem [Moses] requires that the environment structure be potentially tree-like.
Finally, the environment at any point in a computation can never be deeper than the lexical depth of the expression being evaluated at that time; i.e., the environment contains bindings only for variables bound in lambdas lexically surrounding the expression being evaluated. This is true even if recursive functions are involved. ……Furthermore, it is not even necessary to scan the environment for the variable, since its value must be in a known position relative to the top of the environment structure; this position can be computed by a compiler at compile time on the basis of lexical scope.
The variables in a lambda expression are dummy or bound variables because systematically changing them does not alter the meaning of the expression. Thusλ[[u;v];v2+u]
means the same thing asλ[[x;y];y2+x]
.
We shall sometimes use expressions in which a variable is not bound by a lambda. For example, in the function of two variablesλ[[x;y];xn+yn]
the variablen
is not bound. This is called a free variable. It may be regarded as a parameter. Unlessn
has been given a value before trying to compute with this function, the value of the function must be undefined. ……
When the compiler is called upon to compile a function, it looks for anEXPR
orFEXPR
on the property list of the function name. The compiler then translates this S-expression into an S-expression that represents a subroutine in the LISP Assembly Language (LAP). LAP then proceeds to assemble this program into binary program space. Thus anEXPR
, or anFEXPR
, has been changed to aSUBR
or anFSUBR
, respectively. ……
Free variables in compiled functions must be declared before the function is compiled. ……
A variable is bound in a particular function when it occurs in a list of bound variables following the wordLAMBDA
orPROG
. Any variable that is not bound is free. ……
When a variable is used free, it must have been bound by a higher level function. If a program is being run interpretively, and a free variable is used without having bee bound on a higher level, error diagnostic *A 8* will occur.
If the program is being run complied, the diagnostic may not occur, and the variable may have value NIL.
There are three types of variables in compiled functions: ordinary variables,SPECIAL
variables, andCOMMON
variables.SPECIAL
andCOMMON
variables must be declared before compiling. Any variable that is not declared will be considered an ordinary variable.
When functions are translated into subroutines, the concept of a variable is translated into a location where an argument is stored. If the variable is an ordinary one, then a storage location for it is set up on the push-down list. Other functions cannot find this private cell, making it impossible to use it as a free variable.SPECIAL
variables have the indicatorSPECIAL
on their property lists Following the indicator there is a pointer to a fixed cell. When this variable is bound, the old value is saved on the push-down list, and the current value is stored in theSPECLAL
cell. When it is no longer bound, the old value must be restored. When a function uses this variable free, then the quantity in theSPECIAL
cell is picked up.SPECIAL
variables are declared by using the pseudo-functionspecial[a]
, wherea
is a list of variable names. ……SPECIAL
variables are inexpensive and will allow free communication among compiled functions. They do not increase run time significantly.SPECIAL
variables cannot be communicated between the interpreter and compiled functions.COMMON
variables have the flagCOMMON
on their property lists; however, this is only used to inform the compiler that they areCOMMON
, and is not needed at run time.COMMON
variables are bound on an a-list by the compiled functions. When they are to be evaluated,eval
is given this a-list. This happens at run time.
The use ofCOMMON
variables will slow down any compiled function using them. However, they do provide complete communication between interpreted and compiled functions.COMMON
variables are declared bycommon[a]
, wherea
is a list of variable names. ……
LISP is designed in such a way that all functions for which the possibility of recursion can exist are in fact recursive. This requires that all temporary stored results related to the computation that is in progress be set aside when a piece of coding is to be used recursively, and that they be later restored. This is done automatically and need not be programmed explicitly.
All saving of temporary results in LISP is performed on a linear block of storage called the push-down list. Each set of stored data that is moved onto the push-down list is in a block labeled with its size and the name of the subroutine from which it came. Since it is in the nature of recursion that the first block to be saved is always the last block to be restored, it is possible to keep the push-down list compact.
There are several ways in which to maintain the values of free variables (and, thus the computational environment) in order to handle cases like the one above. All of the techniques used involve some increase in time. One approach makes it easy to access the value of a free variable is local. This approach is favored in recent implementations of LISP. We shall call it the "shallow access" approach.
Another approach is to make it relatively easy to enter and leave a block in which a free variable is bound, but relatively expensive to access a free variable. This approach is used in the classical "alist" implementation of LISP. Several Algol systems have opted for a similar approach. We shall call this the "deep access" approach. Both of these approaches allow one to modify the value of the variable as well as access it. The access time is approximately the same as modification time in both approaches.
Let us consider the "shallow access" approach first. By "shallow access" we mean that the value of a free variable can be obtained in a single fetch from memory. Since the current value may be stored in a difficult-to-determine location up in the stack, a special cell for its current value is used. In many recent LISP implementations this special values cell is stored as a property of the atom structure, and is unique to the free variable. In order to maintain the proper value in the special cell, extra work must be done every time a function or a block is entered in which the variable is an argument or is local. On entering such a function or block one usually saves the old value which is in the special cell on the stack alone with sufficient information to allow one to know from where the stack value came. On leaving such a function or block one must remember to store the value saved in the stack in the special cell.
The "deep access" approach forces one to search upward in the stack for the most recent value of the free variable. Such a search may be quite deep and slow if the free variable was bond vary far up in the stack. However, this approach has the advantage of avoiding the need for an extra special value cell. In addition, we shall see later that this approach sometimes allows one to use free variables with greater ease and flexibility than the "shallow access" approach. ……
Certain LISP systems use "deep access" in interpreted functions and "shallow access" in compiled functions. if free variables in compiled functions are declared to beCOMMON
, their values will be stored on the "alist". In this manner one can solve the environment problem. The cost is a very slow interpreter and "deep access" to free variables.
(ENCLOSE fnrep envrep)
ENCLOSE
takes two s-expressions, one representing the code for a procedure, and the other representing the (lexical) environment in which is to run.ENCLOSE
returns a (closed) procedure which can be invoked.
The representation of the code is the standard s-expression description (a lambda-expression). the representation of the environment is an association list (a-list) of the traditional kind:
((var1 . value1) (var2 . value2) ...)
NIL
represents the global lexical environment. ……
TheEVALUATE
primitive described in [SCHEME] has been removed from the language. We discovered (the hard way) that the straightforward implementation ofEVALUATE
(evaluate the given expression in the current environment) destroys referential transparency. We then altered it to evaluate the expression in the top-level environment, but were still disturbed by the extent to which one is tied to a particular representation of a procedure to be executed.
Inspired by ACTORS [Greif and Hewitt][Smith and Hewitt], we have implemented an interpreter for a LISP-like language, SCHEME, based on the lambda calculus [Church], but extended for side effects, multiprocessing, and process synchronization. The purpose of this implementation is tutorial. We wish to:
⑴ alleviate the confusion caused by Micro-PLANNER, CONNIVER, etc. by clarifying the embedding of non-recursive control structures in a recursive host language like LISP.
⑵ explain how to use these control structures, independent of such issues as pattern matching and data base manipulation.
⑶ have a simple concrete experimental domain for certain issues of programming semantics and style.
There are several important consequences of closing every lambda expression in the environment from which it is passed (i.e., in its "lexical" or "static" environment).
First, the axioms of lambda calculus are automatically preserved. Thus, referential transparency is enforced. This in turn implies that there are no "fluid" variable bindings (as there are in standard stack implementations of LISP such as MacLISP).
Second, the upward funarg problem [Moses] requires that the environment structure be potentially tree-like.
Finally, the environment at any point in a computation can never be deeper than the lexical depth of the expression being evaluated at that time; i.e., the environment contains bindings only for variables bound in lambdas lexically surrounding the expression being evaluated. This is true even if recursive functions are involved. ……Furthermore, it is not even necessary to scan the environment for the variable, since its value must be in a known position relative to the top of the environment structure; this position can be computed by a compiler at compile time on the basis of lexical scope.
DEFINE
- This is analogous to the MacLISPDEFUN
primitive (but note that theLAMBDA
must appear explicitly!). It is used for defining a function in the "global environment" permanently, as opposed toLABELS
(see below), which is used for temporary definitions in a local environment.DEFINE
takes a name and a lambda expression; it closes the lambda expression in the global environment and stores the closure in the LISP value cell of the name (which is a LISP atom). ……LABELS
- We have decided not to use the traditionalLABEL
primitive in this interpreter because it is difficult to define several mutually recursive functions using onlyLABEL
. The solution, which Hewitt [Smith and Hewitt] also uses, is to adopt an ALGOL-esque block syntax:
(LABELS <function definition list> <expression>)
This has the effect of evaluating the expression in an environment where all the functions are defined as specified by the definitions list. Furthermore, the functions are themselves closed in that environment, and not in the outer environment; this allows the functions to call themselves and each other recursively.
Atoms which are not atomic symbols (identifiers) evaluate to themselves. Typical examples of such atoms are numbers, arrays, and strings (character arrays). Symbols are treated as identifiers or variables. They may be lexically bound by lambda-expressions. There is a global environment containing values initially have as their values primitive operations such as, for example,CAR
,CONS
, andPLUS
. SCHEME differs from most LISP systems in that the atomCAR
is not itself an operation (in the sense of being an invocable object, e.g. a valid first argument toAPPLY
), but only has one as a value when considered as an identifier.
Lambda calculus (and related languages, such as "pure LISP") is often used for modelling the applicative constructs of programming languages. However, they are generally thought of as inappropriate for modelling imperative constructs. …… we show how imperative constructs may be modelled by applicative SCHEME constructs. ……
Up to now we have thought of SCHEME’sLAMBDA
expressions as functions, and of a combination such as(G (F X Y))
as meaning “apply the functionF
to the values ofX
andY
, and return a value so that the functionG
can be applied and return a value ...” But notice that we have seldom usedLAMBDA
expressions as functions. Rather, we have used them as control structures and environment modifiers. For example, consider the expression:
(BLOCK (PRINT 3) (PRINT 4))
This is defined to be an abbreviation for:
((LAMBDA (DUMMY) (PRINT 4)) (PRINT 3))
We do not care about the value of thisBLOCK
expression; it follows that we do not care about the value of the(LAMBDA (DUMMY) ...)
. We are not usingLAMBDA
as a function at all.
It is possible to write useful programs in terms ofLAMBDA
expressions in which we never care about the value of any lambda expression. We have already demonstrated how one could represent any “FORTRAN” program in these terms: all one needs isPROG
(withGO
andSETQ
), and
…… we will consider the problem of dynamically scoped, or "fluid", variables. These do not exist in ALGOL, but are typical of many LISP implementations, ECL, and APL. We will see that fluid variables may be modelled in more than one way, and that one of these is closely related to continuation-passing.
It is always possible, ……to perform any calculation in this way: rather than reducing to its value, it reduces to an application of a continuation to its value (cf. [Fischer]). That is, in this continuation-passing programming style, a function always "returns" its result by "sending" it to another function.
BLOCK
- This is like the MacLISPPROGN
, but arranges to evaluate its last argument without an extra net control frame (explained later), so that the last argument may involved in an iteration. Note that in SCHEME, unlike MacLISP, the body of aLAMBDA
expression is not an implicitPROGN
.
At this point we are almost in a position to model the most general form of compound statement. In LISP, this is called the "PROG
feature". In addition to statement sequencing andGO TO
statements, one can return a value from aPROG
by using theRETURN
statement.
Let us first consider the simplest compound statement, which in SCHEME we callBLOCK
. Recall that(BLOCK S1 S2)
is defined to be((LAMBDA (DUMMY) S2) S1)
Notice that this not only performsS1
beforeS2
, but also returns the value ofS2
. Furthermore, we defined(BLOCK S1 S2 ... Sn)
so that it returns the value ofSn
. ThusBLOCK
may be used as a compound expression, and models a LISPPROGN
, which is aPROG
with noGO TO
statements and an implicitRETURN
of the last "statement" (really an expression).
(BLOCK x1 x2 ... xn)
(BLOCK x) → x
(BLOCK x . r) → ((LAMBDA (A B) (B)) x (LAMBDA () (BLOCK . r)))BLOCK
sequentially evaluates the subformsxi
from left to right. For example:(BLOCK (ASET' X 43) (PRINT X) (+ X 1))
returns44
after settingx
to43
and then printing it {NoteBLOCK
Exploits Applicative Order}.(LET ((v1 x1) (v2 x2) ... (vn xn)) . body)
→ ((LAMBDA (v1 v2 ... vn) (BLOCK . body)) x1 x2 ... xn)LET
provides a convenient syntax for binding several variables to corresponding quantities. It allows the forms for the quantities to appear textually adjacent to their corresponding variables. Notice that the variables are all bound simultaneously, not sequentially, and that the initialization formxi
may be evaluated in any order. For convenience,LET
also supplies aBLOCK
around the forms constituting its body.
DO
- This is like the MacLISP "new-style"DO
; old-styleDO
is not supported.
CATCH
- This is the "escape operator" which gives the user a handle on the control structure of the interpreter. The expression:
(CATCH <identifier> <expression>)
evaluates<expression>
in an environment where<identifier>
is bound to a continuation which is "just about to return from theCATCH
"; that is, if the continuation is called as a function of one argument, then control proceeds as if theCATCH
expression had returned with the supplied (evaluated) argument as its value. ……
As another example, we can define aTHROW
function, which may then be used withCATCH
much as they are in LISP:
(DEFINE THROW (LAMBDA (TAG RESULT) (TAG RESULT)))
EVALUATE
- This is similar to the LISP functionEVAL
. It evaluates its argument, and then evaluates the resulting s-expression as SCHEME code.