求值器完整实现代码我已经上传到了GitHub仓库:TinySCM,感兴趣的童鞋可以前往查看。这里顺便强烈推荐UC Berkeley的同名课程CS 61A。
在这个层次结构的最底层是对象语言。对象语言只涉及特定的域,而不涉及对象语言本身(比如它们的文法规则,或其中的其体句子)。如要涉及它们,则要有一种元语言。对于语言的两个层次这一经验,所有学习外国语的人都是很熟悉的。然后,就要有一种元元语言来讨论元语言,以此类推。
——侯世达《哥德尔、埃舍尔、巴赫:集异璧之大成》
到目前为止,我们探讨的都是通过过程抽象、数据抽象以及模块化等手段来控制系统的复杂性。为了阐释这些技术,我们一直使用的是同一种编程语言。然而,随着我们所面对的问题更复杂,我们必须经常转向新的语言,以便能够有效地表述自己的想法。实际上,建立新语言是工程设计中控制复杂性的一种威力强大的工作策略,我们常常能通过采用一种新语言而处理复杂问题的能力。
元语言抽象就是建立新的语言。它在工程设计的所有分支中都扮演着重要的角色,在计算机程序设计领域更是特别重要。因为这个领域中,我们不仅可以设计新的语言,还可以通过构造求值器的方式实现这些语言。对某个程序设计语言的求值器(或者解释器)也是一个过程,在应用于这个语言的一个表达式时,它能够执行求值这个表达式所要求的动作。
把这一点看做是程序设计语言中最基本的思想一点也不过分:
求值器决定了一个程序设计语言中各种表达式的意义,而它本身也不过就是另一个程序。
事实上,我们几乎可以把任何程序看做是某个语言的求值器。比如在符号代数中,一个多项式系统包含着多项式的算数规则以及底层的实现,一个符号求导系统亦包含着函数求导的规则以及底层的实现(参见我之前的博客《SICP:符号求导、集合表示和Huffman树(Python实现)》)。从这样一种观点看,处理大规模计算机系统的技术,与构造新的程序设计语言的技术有紧密的联系,而计算机科学中一个重要的思想就是如何去构造适当的描述语言。
接下来我们将要讨论如何关于在一些语言的基础上构造新的语言。在这篇博客里,我们将用Python语言去构造一个Scheme语言的求值器。事实上求值器的实现语言无关紧要,我们也可以用Scheme语言去构造Scheme语言的求值器。用于被求值语言同样的语言写出来的求值器被称为元循环(metacircular)。
从根本上说,元循环求值器也就是我们在博客《SICP:赋值和局部状态(Python实现)》中所描述求值的环境模型的一个实现形式。回忆一下,该模型包括两个部分:
这两条规则描述了求值过程的核心部分,也就是它的基本循环。在这一循环中,表达式在环境中的求值被规约到过程对实参的应用,而这种应用又被规约到新的表达式在新的环境中的求值。如此下去,直至我们下降到符号(其值可以在环境中找到)或基本过程(它们可以直接应用)。 如下图所示:
这一求值循环实际体现为求值器里的两个关键函数scheme_eval()
和scheme_apply()
的相互作用,我们在4.1.1节将描述他们。
求值器的实现依赖于一些定义了被求值表达式的 语法(syntax) (也即被求值表达式是以何种数据结构来表示的)的过程。我们仍将采用自顶向下的数据抽象技术,设法使求值器独立于语言的具体表示。例如,对于赋值表达式(set! <var> <val>)
,我们用抽象的选取函数assignment_variable()
和assignment_value()
去访问赋值中相应的部分(不过这里需要注意,为了方便后面的类型分派操作,我们仍然假设可以用first_expr()
和rest_exprs()
分别去访问表达式的首元素和其余元素,算是对抽象性的一个小小的击穿吧)。表达式的具体表示将在4.1.2节里描述。在4.1.3里还会描述一些过程和环境的表示形式,如Environment
类、LambdaProcedure
类的定义及其对应的一些方法。
求值过程可以描述为两个函数scheme_eval()
和scheme_apply()
之间的相互作用(interplay)。
scheme_eval()
的参数是一个表达式和一个环境。scheme_eval()
对表达式进行分类以此展开后续的求值工作。正如我们上面所说,我们采用选取函数first_expr()
和rest_exprs()
分别去访问表达式的首元素和其余元素,并根据表达式的首元素(也即其标签)来完成表达式类型的判定。表达式的类型可做如下分类:
基本表达式:
scheme_eval()
直接返回这个表达式本身。scheme_eval()
必须在环境中查找变量的值。特殊形式(special forms):
if
表达式、变量的赋值/定义、引号('
)表达式、lambda表达式等、我们的处理方式是将其分派给不同的名为eval_×××()
的求值函数处理。如if
表达式就分派给eval_if()
函数来处理。组合式(combinations):
scheme_eval()
必须先递归地求值组合式的运算符部分和运算对象部分。而后将这样得到的过程对象和参数送给scheme_apply()
,由它去处理实际的过程应用。注意,如果运算符部分是宏(Macro),则不需要事先对其进行求值就直接将其应用,待应用完毕之后再进行求值。下面是scheme_eval()
的定义:
@primitive("eval", use_env=True)
def scheme_eval(expr, env, _=None):
# Evaluate self-evaluating expressions
if is_self_evaluating(expr):
return expr
# Evaluate variables
elif is_scheme_variable(expr):
return env.lookup_variable_value(expr)
# All valid non-atomic expressions are lists (combinations)
if not is_scheme_pair(expr):
raise SchemeError(
"Unknown expression type: {0}".format(repl_str(expr)))
first, rest = first_expr(expr), rest_exprs(expr)
# Evaluate special forms
if is_scheme_symbol(first) and first in SPECIAL_FORMS:
return SPECIAL_FORMS[first](rest, env)
# Evaluate an application
else:
operator = scheme_eval(first, env)
# Check if the operator is a macro
if isinstance(operator, MacroProcedure):
return scheme_eval(complete_apply(operator, rest, env), env)
operands = rest.map(lambda x: scheme_eval(x, env))
return scheme_apply(operator, operands, env)
@primitive("eval", use_env=True)
表示将其加入Scheme的基本过程中,过程名为eval
(注意,Python语言中的装饰器会在被装饰的函数定义之后立即运行,故上述操作是在scheme_eval()
函数定义之后立即完成的)。这里的特殊形式的类型分派采用了数据导向的方式(这种做法可以使用户更容易增加scheme_eval()
能分辨的表达式类型,而又不必修改scheme_eval()
的定义本身)。所有的SPECIAL_FORMS
及其所分派的求值函数如下所示:
SPECIAL_FORMS = {
# Conditionals
"if": eval_if,
"cond": eval_cond,
"and": eval_and,
"or": eval_or,
# Sequencing
"begin": eval_begin,
"let": eval_let,
# Assignments
"set!": eval_assignment,
# Definitions
"define": eval_definition,
# Lambda expressions
"lambda": eval_lambda,
# Quoting
"quote": eval_quote,
"unquote": eval_unquote,
"quasiquote": eval_quasiquote,
# Dynamic scoping
"dlambda": eval_dlambda_form,
# Macro
"define-macro": eval_macro_definition,
# Stream
"delay": eval_delay,
"cons-stream": eval_cons_stream
}
注意这里的dlambda
特殊形式定义的是采用动态作用域的过程,是原Scheme标准中没有的。
scheme_apply()
有两个参数,一个是过程,一个是该过程去应用的实参表。scheme_apply()
将过程分为两类:基本过程(求值器内置)和复合过程(通过define
或lambda
/dlambda
特殊形式来定义)。对于基本过程,直接调用该过程对象的apply()
方法去应用实参,对于复合过程,则在新建环境后(该环境包含了形参绑定到实参的新帧),顺序地求值组成该过程体的那些表达式。下面是scheme_apply()
的定义:
@primitive("apply", use_env=True)
def scheme_apply(procedure, arguments, env):
validate_procedure(procedure)
if is_primitive_procedure(procedure):
return procedure.apply(arguments, env)
elif is_compound_procedure(procedure):
new_env = procedure.make_call_frame(arguments, env)
# Note that `tail` is set to True when `eval_sequence()` is called here
return eval_sequence(procedure.body, new_env, tail=True)
else:
raise SchemeError(
"Unknown procedure type: {0}".format(repl_str(procedure)))
eval_if()
先在给定的环境中求值if
表达式的谓词(predicate)部分,如果得到的结果为真,eval_if()
就去求值这个if
的推论(consequent)部分,否则就求值其替代(alternative)部分。其中需要注意,对于if
语句是肯定有推论部分的,但是我们后面会将cond
语句的求值规约到对if
语句的求值,因cond
语句推论可能为空,故此处需要做出判断——若推论为空,则返回谓词的求值结果,否则照常对推论求值。此外,如果if
语句没有替代部分,则返回False
。
def eval_if(expr, env, tail=True):
validate_form(expr, min=2, max=3)
if_pred = if_predicate(expr)
if_predicate_val = scheme_eval(if_pred, env)
# All values in Scheme are true except `false` object,
# that is why we need `is_scheme_true()`
if is_scheme_true(if_predicate_val):
# Note that for `if` caluse , it muse have consequnet,
# so `if_consequent` can not be None (although it can be `nil`).
# But for `cond` clause, `if_consequent` can be None.
if_conseq = if_consequent(expr)
if if_conseq is None:
return if_predicate_val
else:
return scheme_eval(if_conseq, env, tail=tail)
# Turn to alternative
elif len(expr) == 3:
return scheme_eval(if_alternative(expr), env, tail=tail)
# If there is no alternative, return False
else:
return False
从eval_if()
对is_scheme_true()
的使用中,我们可以看到被实现语言(Scheme)和实现所用的语言(Python)之间的联系。if_predicate()
在被实现的语言里求值,产出这一语言里的一个值,而解释器的谓词is_scheme_true()
实际上会将该值翻译为可由实现所用语言的if
检测的值。这里因为在Scheme中,除了False
之外的对象都应被判断为真,故我们实际上需要用Python将is_scheme_true()
实现如下:
def is_scheme_true(val):
return val is not False
注意这里如果对象为真则返回该对象本身,而不是直接返回True
。
至于cond
表达式,可以做为派生表达式(derived expressions)由if
表达式实现出来,而不必直接去实现。
def eval_cond(expr, env, tail=True):
return scheme_eval(cond_to_if(expr), env, tail)
最后,我们还需要实现and
和or
这两个布尔表达式:
def eval_and(exprs, env, tail=True):
# If there is no expression to be evaluated, return True
if exprs is nil:
return True
# If the last expression is reached (indicating that the values of the
# previous expressions are all true), then the evaluation result is
# returned directly
elif rest_exprs(exprs) is nil:
return scheme_eval(first_expr(exprs), env, tail=tail)
value = scheme_eval(first_expr(exprs), env)
# If an expression evaluates to False, return False,
# and the remaining expressions are not evaluated
if is_scheme_false(value):
return False
else:
# If an expression evaluates to True, go on,
return eval_and(rest_exprs(exprs), env)
def eval_or(exprs, env, tail=True):
# If there is no expression to be evaluated, return True
if exprs is nil:
return False
# If the last expression is reached (indicating that the values of the
# previous expressions are all False), then the evaluation result is
# returned directly
elif rest_exprs(exprs) is nil:
return scheme_eval(first_expr(exprs), env, tail=tail)
value = scheme_eval(first_expr(exprs), env)
# If an expression evaluates to True, return value, and the remaining
# expressions are not evaluated
if is_scheme_true(value):
return value
else:
return eval_or(rest_exprs(exprs), env)
如上所示,and
和or
表达式都是短路(short-circuited)的。对于and
表达式,如果其子表达式有求值为False
的,则直接返回False
;对于or
表达式,如果其子表达式有求值为True
的,直接返回True
。
eval_sequence()
用在scheme_apply()
里,用于求值过程体里的表达式序列。它也用在scheme_eval()
里,用于求值begin
和let
表达式内的表达式序列。这个过程以一个表达式序列和一个环境为参数,按照序列里表达式出现的顺序对它们进行求值,并返回最后一个表达式的值。
def eval_sequence(exprs, env, tail=False):
if not is_scheme_pair(exprs):
return
# If `exprs` is the last expression
if rest_exprs(exprs) is nil:
# The value of the last expression is returned as the value of the
# entire `begin` special form(or the body of a procedure)
return scheme_eval(first_expr(exprs), env, tail)
else:
# Evaluate the expressions <expr 1>, <expr 2>, ..., <expr k> in order
scheme_eval(first_expr(exprs), env)
return eval_sequence(rest_exprs(exprs), env, tail)
eval_begin()
和eval_let()
亦依据eval_sequence()
来定义:
def eval_begin(exprs, env, tail=True):
validate_form(exprs, min=1)
return eval_sequence(exprs, env, tail)
def eval_let(exprs, env, tail=True):
validate_form(exprs, min=2)
let_env = make_let_env(first_expr(exprs), env)
return eval_sequence(rest_exprs(exprs), let_env, tail=tail)
下面过程处理变量赋值,它先调用scheme_eval()
求出需要赋的值,将变量和得到的值传给env.set_variable_value()
方法,将有关的值安置到环境env
里。
def eval_assignment(expr, env):
env.set_variable_value(assignment_variable(
expr), scheme_eval(assignment_value(expr), env))
变量定义也用类似方式处理:
def eval_definition(expr, env):
# Check that expressions is a list of length at least 2
validate_form(expr, min=2)
var = definition_varaible(expr)
val = definition_value(expr)
env.define_variable(var,
scheme_eval(val, env))
return var
对lambda表达式进行求值时(注意此时只是对lambda表达式的”定义“进行求值),我们会先分别获取lambda表达式的形参和过程体,并结合lambda表达式定义时的环境来初始化LambdaProcedure
对象(也即默认采用的基于词法作用域规则)。
def eval_lambda(expr, env):
validate_form(expr, min=2)
parameters = lambda_parameters(expr)
validate_parameters(parameters)
body = lambda_body(expr)
return LambdaProcedure(parameters, body, env)
而对于我们额外添加的采用动态作用域的dlambda表达式,在初始化时则不会用到其定义时的环境。
def eval_dlambda_form(expr, env):
validate_form(expr, min=2)
parameters = expr.first
validate_parameters(parameters)
return DLambdaProcedure(parameters, expr.rest)
对quote
表达式(即'
)进行求值时,直接返回其所引用的表达式本身即可(无需环境)。
def eval_quote(expr, env):
validate_form(expr, min=1, max=1)
return text_of_quotation(expr)
对quasiquote
表达式(即 ` )进行求值则是一个递归的过程,在获得了 ` 所引用的表达式后,我们需要扫描该表达式查看是否有被unquote(即以,
开头)的子表达式,如果遇到了则对该子表达式求值,而其余表达式则保持不求值状态。比如(1 2 ,(+ 3 4))
的求值结果为'(1 2 7)
。注意这里quasiquote
内部可以嵌套多个quasiquote
和unquote
,故需要在递归时维护depth
变量来表示被unquote操作“抵消”后剩余的quasiquote
深度。
def eval_quasiquote(expr, env):
def quasiquote_item(val, env, depth):
"""Evaluate Scheme expression `val` that is nested at depth `level` in
a quasiquote form in frame `env`."""
if not is_scheme_pair(val):
return val
# When encountering `unquote`, we decrease the depth by 1.
# If the depth is 0, we evaluate the rest expressions.
if is_tagged_list(val, "unquote"):
depth -= 1
if depth == 0:
expr = rest_exprs(val)
validate_form(expr, 1, 1)
return scheme_eval(first_expr(expr), env)
elif val.first == "quasiquote":
# Leave the item unevaluated
depth += 1
# Recursively quasiquote the items of the list
return val.map(lambda elem: quasiquote_item(elem, env, depth))
validate_form(expr, min=1, max=1)
# Note that when call `quasiquote_item`, we have encountered
# the first quasiquote, so depth=1
return quasiquote_item(text_of_quotation(expr), env, depth=1)
注意unquote操作是不能够在quasiquote
表达式之外进行的,如果在quasiquote
之外对unquote进行求值,则会报对应的错误:
def eval_unquote(expr, env):
raise SchemeError("Unquote outside of quasiquote")
对宏的定义,也即define-macro
表达式进行求值时,我们选择将表达式的形参、表达式体及环境都保存到MacroProcedure
对象中,而不立即进行求值操作求值。此外,我们还在环境中添加宏的名称与MacroProcedure
对象的绑定。
def eval_macro_definition(expr, env):
validate_form(expr, min=2)
target = expr.first
if is_scheme_pair(target) and is_scheme_symbol(target.first):
func_name = target.first
# `target.rest` is parameters,not `target.rest.first`
parameters = target.rest
body = expr.rest
# Just store the expression, rather than evaluate it
env.define_variable(func_name, MacroProcedure(parameters, body, env))
return func_name
else:
raise SchemeError("Invalid use of macro")
这个求值器很像我们在博客《SICP:符号求导、集合表示和Huffman树(Python实现) 》中所讨论的符号微分程序。这两个程序完成的都是一些对符号表达式的操作。在这两个程序中,对一个复合表达式的求值结果,也是由对表达式片段的递归操作来确定的,且对该结果的组合方式也是由表达式的类型来确定。在这两个程序中,我们都采用了数据抽象技术,将一般性的操作规则与表达式的表示方式进行了解耦(decouple)。在符号微分程序中,这意味着同一个微分程序可以处理前缀(prefix)形式、中缀(infix)形式或其他形式的代数表达式。对于求值器,这意味着被求值语言的语法(syntax)仅仅由对表达式进行分类和片段提取的过程来决定。
这里需要提一下,我们求值器的输入是以序对
Pair
组成的表,也即经过词法分析(lexical analysis)+语法分析(syntactic analysis)后所构建的抽象语法树(abstract syntax tree, AST)。比如对于 Scheme表达式(+ (* 3 5) (- 10 6))
其对应的表为:
Pair('+', Pair(Pair('*', Pair(3, Pair(5, nil))), Pair(Pair('-', Pair(10, Pair(6, nil))), nil)))
事实上由于Scheme程序本身就可视作一棵AST,因此为Scheme程序写用于语法分析的Parser异常简单。我们可以通过
Pair
对象的.first
属性访问其的第一个元素,.rest
属性访问其的后续元素。
下面是我们所要实现的Scheme语言的语法规范:
nil
(也即空表'()
)、布尔值和undefined
(在Python中表示为None
):def is_self_evaluating(expr):
return is_scheme_boolean(expr) or is_scheme_number(expr) or \
is_scheme_null(expr) or is_scheme_string(expr) or expr is None
def is_scheme_variable(x):
return is_scheme_symbol(x)
(quote <text-of-quotation>)
(求值器看到的表达式是以quote
开头的表,即使这种表达式在输入时用的是一个引号):def text_of_quotation(expr):
return expr.first
def is_unquote(expr):
return is_tagged_list(expr, "unquote")
注意这里对text_of_quotation()
函数而言传入的expr
是不含标签quote
的,故直接使用expr.first
来访问<text-of-quotation>
即可。
is_unquote()
函数借助函数is_tagged_list()
来定义,它可用于确定一个表的开始是不是某个给定符号:
def is_tagged_list(expr, tag):
if is_scheme_pair(expr):
return expr.first == tag
else:
return False
注意我们这里没有is_quote()
函数,这是因为对quote
的类型判断和其它特殊形式一样,直接在scheme_eval()
的分派过程中解决了。
(set! <var> <value>)
:def assignment_variable(expr):
return expr.first
def assignment_value(expr):
return expr.rest.first
同样,注意此处的expr
不含标签。
(define <var> <value>)
或者
(define (<var> <parameter 1>... <parameter n>)
<body>)
后一形式(标准的过程定义)只是下面形式的一种语法包装:
(define <var>
(lambda (<parameter 1> ... <parameter n>)
<body>))
相应的语法函数是
def definition_varaible(expr):
target = expr.first
# For the case of (define <var> <value>)
if is_scheme_symbol(target):
# `(define x)` or `(define x 2 y 4)` is invalid
validate_form(expr, min=2, max=2)
return target
# For the case of (define (<var> <param 1>, ..., <param n>) <body>)
elif is_scheme_pair(target) and is_scheme_symbol(target.first):
return target.first
else:
bad_target = target.first if is_scheme_pair(target) else target
raise SchemeError("Non-symbol: {0}".format(bad_target))
def definition_value(expr):
target = expr.first
# For the case of (define <var> <value>)
if is_scheme_symbol(target):
return expr.rest.first
# For the case of (define (<var> <param 1>, ..., <param n>) <body>)
elif is_scheme_pair(target) and is_scheme_symbol(target.first):
# Note: The validation of the lambda special form is turned over
# to `scheme_eval()`
return make_lambda(target.rest, expr.rest)
else:
bad_target = target.first if is_scheme_pair(target) else target
raise SchemeError("Non-symbol: {0}".format(bad_target))
同样,注意此处的expr
不含标签。
lambda
开始的表:def lambda_parameters(expr):
return expr.first
def lambda_body(expr):
return expr.rest
同样,注意此处的expr
不含标签。
我们还为lambda
表达式提供了一个构造函数,它用在上面的definition_value()
里。
def make_lambda(parameters, body):
return scheme_cons("lambda", scheme_cons(parameters, body))
if
开始,有一个谓词部分,一个推论部分和一个(可缺的)替代部分。如果这一表达式没有替代部分,我们就用False
做为其替代。def if_predicate(expr):
return expr.first
def if_consequent(expr):
return expr.rest.first
def if_alternative(expr):
return expr.rest.rest.first
我们也为if
表达式提供了一个构造函数,它在cond_to_if
中使用,用于将cond
表达式变换为if
表达式。
def make_if(predicate, consequent, alternative):
return scheme_list("if", predicate, consequent, alternative)
begin
包装起一个表达式序列。这里我们设计选择函数返回序列中的第一个表达式和其余表达式。def first_expr(seq):
return seq.first
def rest_exprs(seq):
return seq.rest
我们还设计了一个构造函数sequence_to_expr()
(用在cond_to_if
里),它把一个序列变换为一个表达式,如果需要的话就加上begin
作为开头:
def sequence_to_expr(seq):
if seq is nil:
return seq
elif rest_exprs(seq) is nil:
return first_expr(seq)
else:
return make_begin(seq)
def make_begin(seq):
return scheme_cons("begin", seq)
let
表达式在规约到用eval_sequence()
对表达式序列进行求值之前,需要新建环境。该新环境的构造函数下:def make_let_env(bindings, env):
def bindings_items(bindings, env):
if bindings is nil:
return nil, nil
binding = bindings.first
validate_form(binding, min=2, max=2)
var = binding.first
val = scheme_eval(binding.rest.first, env)
vars, vals = bindings_items(bindings.rest, env)
return scheme_cons(var, vars), scheme_cons(val, vals)
if not is_scheme_list(bindings):
raise SchemeError("Bad bindings list in let form")
vars, vals = bindings_items(bindings, env)
validate_parameters(vars)
return env.extend_environment(vars, vals)
派生表达式
在一些语言中,一些特殊形式可以基于其他特殊形式的表达式定义出来,而不必直接去实现,比如cond
表达式和let
表达式。这样采用语法变换的方式实现的表达式称为派生表达式(derived expressions)。
cond
表达式可以实现为一些嵌套的if
表达式。例如,我们可以将对于下列表达式的求值问题:(cond ((> x 0) x)
((= x 0) (display 'zero) 0)
(else (- x)))
规约为对下面涉及if
和begin
的表达式的求值问题:
(if (> x 0)
x
(if (= x 0)
(begin (display 'zero)
0)
(- x)))
采用这种方式实现对cond
的求值能简化求值器。
下面展示了提取cond
表达式中各个部分的语法过程,以及函数cond_to_if()
,它能将cond
表达式变换为if
表达式。一个分情况分析以cond
开始,并包含一个谓词-动作子句的表。如果一个子句的符号是else
,那么就是一个else
子句。
def cond_predicate(clause):
return clause.first
def cond_actions(clause):
return clause.rest
def cond_to_if(exprs):
return expand_clauses(exprs)
def expand_clauses(clauses):
# return None means that interpreter does not print anything
if clauses is nil:
return None
first = clauses.first
rest = clauses.rest
validate_form(first, min=1)
if cond_predicate(first) == "else":
if rest is nil:
return sequence_to_expr(first.rest)
else:
raise SchemeError(
"ELSE clause is not last: {0}".format(
repl_str(clauses)))
else:
if cond_actions(first) is nil: # for example, (cond ((= 1 1)))
# there is no consequent, we denote it as None
# o distinguish it from nil
if_consequent = None
else: # for example, (cond ((= 1 1) 2)) or (cond ((= 1 1) nil))
# there is a consequent, including nil
if_consequent = sequence_to_expr(first.rest)
return make_if(cond_predicate(first), if_consequent, cond_to_if(
rest))
除了需要定义表达式的外部语法之外,求值器的实现还必须定义好在其内部实际操作的数据结构,做为程序执行的一部分。例如序列数据结构,过程和环境的表示形式,真和假的表示方式等等。
序列数据结构
如我们在博客《SICP: 层次性数据和闭包性质(Python实现)》中所言,序列数据结构由序对来进行层次化定义,序对的定义如下:
class Pair:
def __init__(self, first, rest):
self.first = first
self.rest = rest
def __len__(self):
n, rest = 1, self.rest
while isinstance(rest, Pair):
n += 1
rest = rest.rest
# The tail of the list must be nil
if rest is not nil:
raise TypeError("length attempted on improper list")
return n
def __eq__(self, p):
if not isinstance(p, Pair):
return False
return self.first == p.first and self.rest == p.rest
def map(self, fn):
mapped = fn(self.first)
if self.rest is nil or isinstance(self.rest, Pair):
return Pair(mapped, self.rest.map(fn))
else:
raise TypeError("ill-formed list (cdr is a promise)")
def flatmap(self, fn):
from primitive_procs import scheme_append
mapped = fn(self.first)
if self.rest is nil or isinstance(self.rest, Pair):
return scheme_append(mapped, self.rest.flatmap(fn))
else:
raise TypeError("ill-formed list (cdr is a promise)")
此外,我们还需要一个空表类及其实例:
class nil:
def __len__(self):
return 0
def map(self, fn):
return self
def flatmap(self, fn):
return self
nil = nil()
注意,nil
实例将与其相同的类名进行了覆盖,且该空表类自始至终只有一个实例,所以我们可以直接使用is nil
语法来测试某个对象是否为空表。
谓词检测
为了实现条件表达式,我们把除了False
对象之外的所有东西都接受为真:
def is_scheme_true(val):
return val is not False
def is_scheme_false(val):
return val is False
除了谓词检测之外,还有许多诸如is_scheme_pair()
、is_scheme_list()
之类的Scheme内置数据结构检测函数,就不在此处进行一一列举了,大家可直接参考我项目目录下的primitive_procs.py
文件。
过程的表示
过程分为基本过程(primitive procedures)和复合过程(compound procedures,也即由define
语句或lambda
/dlambda
语句定义的过程)。我们先定义基本过程如下:
class Procedure:
class PrimitiveProcedure(Procedure):
import primitive_procs as pprocs
def __init__(self, fn, name="primitive", use_env=False):
self.name = name
self.fn = fn
self.use_env = use_env
def apply(self, arguments, env):
if not self.pprocs.is_scheme_list(arguments):
raise self.pprocs.SchemeError(
"Arguments are not in a list: {0}".format(arguments))
# Convert a Scheme list to a Python list
arguments_list = self.flatten(arguments)
try:
if self.use_env:
return self.fn(*arguments_list, env)
return self.fn(*arguments_list)
except TypeError:
raise self.pprocs.SchemeError(
"Incorrect number of arguments: {0}".format(self))
def flatten(self, arguments):
if arguments is nil:
return []
else:
return [arguments.first] + self.flatten(arguments.rest)
下列函数可检查过程是否为基本过程:
def is_primitive_procedure(procedure):
return isinstance(procedure, PrimitiveProcedure)
复合过程则包括形参parameters
、过程体body
和环境body
:
class LambdaProcedure(Procedure):
import primitive_procs as pprocs
def __init__(self, parameters, body, env):
assert isinstance(env, Environment), "env must be of type Environment"
self.pprocs.validate_type(parameters, self.pprocs.is_scheme_list,
0, "LambdaProcedure")
self.pprocs.validate_type(
body, self.pprocs.is_scheme_list, 1, "LambdaProcedure")
self.parameters = parameters
self.body = body
self.env = env
def make_call_frame(self, arguments, env):
return self.env.extend_environment(self.parameters, arguments)
复合过程的make_call_frame()
方法用于将复合过程对象应用于实参时,向过程定义时的环境self.env
中添加一个新帧(注意不是调用时环境env
,调用时环境env
并未使用),从而扩展得到一个新的环境。具体的env.extend_environment()
方法我们会在环境的表示部分再讲述如何实现。
下列函数可检查过程是否为复合过程:
def is_compound_procedure(procedure):
return isinstance(procedure, Procedure)
使用动态作用域的dlambda过程对象定义如下:
class DLambdaProcedure(Procedure):
def __init__(self, parameters, body):
self.parameters = parameters
self.body = body
def make_call_frame(self, arguments, env):
return env.extend_environment(self.parameters, arguments)
该过程也是复合过程,唯一的区别是在过程应用时,建立的新环境是基于调用时环境env
来扩展的,而不是定义时环境。
环境的表示
求值器还需要定义对环境的表示。正如我们在博客《SICP:求值和环境模型(Python实现)》中所说,一个环境就是一个帧的序列,每个帧都是一个包含绑定的表格,其中绑定关联起一些变量和与之对应的值。
我们首先定义好帧:
class Frame:
def __init__(self):
self.bindings = {}
def add_binding(self, var, val):
self.bindings[var] = val
def set_var(self, var, val):
self.add_binding(var, val)
然后在帧的基础之上定义好环境:
class Environment:
import primitive_procs as pprocs
def __init__(self):
self.frames = self.pprocs.scheme_list(Frame())
def lookup_variable_value(self, var):
...
def extend_environment(self, vars, vals):
...
def define_variable(self, var, val):
...
def set_variable_value(self, var, val):
...
和环境有关的方法lookup_variable_value()
、extend_environment()
、define_variable()
、set_variable_value()
我们在下面进行分别阐述。
lookup_variable_value()
方法返回符号var
在环境里的绑定值,如果这一变量没有绑定就发出一个错误信号:def lookup_variable_value(self, var):
def env_loop(frames):
# If cannot find the variable in the current environment
if self.pprocs.is_scheme_null(frames):
raise self.pprocs.SchemeError(
"Unbound variable: {0}".format(var))
frame = frames.first
if var in frame.bindings.keys():
return frame.bindings[var]
else:
return env_loop(frames.rest)
return env_loop(self.frames)
extend_environment()
方法返回一个新环境,这个环境里包含了一个新帧,其中所有位于表vars
里的符号绑定到表vals
里对应的元素,而该帧的外围环境是当前这个对象所对应的环境。def extend_environment(self, vars, vals):
new_env = Environment()
if len(vars) == len(vals):
new_env.frames = self.pprocs.scheme_cons(
self.make_frame(vars, vals),
self.frames)
elif len(vars) < len(vals):
raise self.pprocs.SchemeError(
"Too many arguemtns supplied")
else:
raise self.pprocs.SchemeError(
"Too few arguemtns supplied")
return new_env
@ staticmethod
def make_frame(vars, vals):
frame = Frame()
while isinstance(vars, Pair):
var = vars.first
val = vals.first
frame.add_binding(var, val)
vars = vars.rest
vals = vals.rest
return frame
define_variable()
方法在当前对象所对应环境的第一个帧里加入一个新绑定,它关联起变量var
和val
。def define_variable(self, var, val):
frame = self.frames.first
frame.add_binding(var, val)
set_variable_value()
方法修改变量var
在当前对象所对应环境里的绑定,使得该变量现在绑定到值val
。如果这一变量没有绑定就发出一个错误信号。def set_variable_value(self, var, val):
def env_loop(frames):
# 空表
if self.pprocs.is_scheme_null(frames):
raise self.pprocs.SchemeError(
"Unbound variable: {0}".format(var))
frame = frames.first
if var in frame.bindings.keys():
frame.set_var(var, val)
return
else:
env_loop(frames.rest)
env_loop(self.frames)
不过需要注意的是,这里所描述的方法,只不过是表示环境的许多方法之一。由于前面采用了数据抽象的技术,而这已经将求值器的其它部分与这些表示细节隔离开,因此如果需要的话我们也完全可以修改环境的表示。在产品质量的Lisp系统里,求值器中环境操作的速度——特别是查找变量的速度——对系统的性能有着重要的影响。这里所描述的表示方式虽然在概念上非常简单,但其工作效率却很低,通常不会用在产品系统里。其低效的原因来自求值器为了找到一个给定变量的绑定,需要搜索许多个帧。这样一种方式称为深约束。避免这一低效性的方法是采用一种称为语法作用域的策略,可参考原书5.5.6节。
有了一个求值器,我们手头上就有了一个有关Lisp表达式如何求值的程序描述(这是用Python描述的)。接下来我们来看如何运行这个程序。
我们的求值器程序最终把表达式规约到基本过程的应用(基本过程的定义在项目代码中的primitive_procs.py
文件中)。而每个基本过程名都必须有一个绑定,以便当scheme_eval()
求值一个应用基本过程的运算符时,可以找到相应的对象,并调用这个对象的apply
方法。为此我们必须创建起一个初始环境,在其中建立起基本过程的名字与一个唯一对象的关联,在求值表达式时的过程中可能遇到这些名字。
def setup_environment():
initial_env = Environment()
initial_env.define_variable("undefined", None)
add_primitives(initial_env, PRIMITIVE_PROCS)
return initial_env
这里的PRIMITIVE_PROCS
是一个Python全局变量,具体而言是一个存储了基本过程函数及其名称的表。基本过程对象的具体表示方式我们已经在上文中提到,也就是PrimitiveProcedure
,因此我们可以用下列函数将基本过程名称及其对象的绑定添加到全局环境中:
def add_primitives(env, funcs_and_names):
for fn, name, use_env in funcs_and_names:
env.define_variable(name, PrimitiveProcedure(
fn, name=name, use_env=use_env))
为了能够很方便地运行这个元循环求值器,我们使用函数read_eval_print_loop()
来模拟Lisp中的读入-求值-打印循环。这个循环打印出一个提示符(prompt),读入输入表达式,进行词法分析和语法分析转换为AST后,再在全局环境里求值这个表达式,而后打印出得到的结果。
def read_eval_print_loop(env, infile_lines=None, interactive=False,
quiet=False, startup=False, load_files=(),
report_errors=False, print_ast=False):
if startup:
for filename in load_files:
scheme_load(filename, True, env)
# Initialize a tokenizer instance
tokenizer = Tokenizer()
# Initialize a parser instance
parser = Parser()
while True:
try:
lines_stream = read_input(infile_lines, input_prompt="scm> ")
# Tokenize the input lines
lines_stream = (tokenizer.tokenize(line) for line in lines_stream)
# Parse a single expression / multiple expressions util all the
# tokens are consumed
while True:
# Parse a complete expression (single-line or multi-line) at a
# time
ast = parser.parse(lines_stream)
result = scheme_eval(ast, env)
if not quiet:
print(repl_str(result))
# If all the tokens read are consumed, then break
if parser.is_empty():
break
except (SchemeError, SyntaxError, ValueError, RuntimeError) as err:
...
def read_input(infile_lines, input_prompt):
if infile_lines: # If use a file stream as input
while infile_lines:
line = infile_lines.pop(0).strip("\n")
yield line
raise EOFError
else: # if use a keyboard stream as input
while True:
yield input(input_prompt)
# If a multi-line expression input is not
# terminated, use whitespace as the
# the input prompt to read more lines.
input_prompt = " " * len(input_prompt)
为了运行这个求值器,现在我们需要着的全部事情就是初始化这个全局环境,并启动上述的读入-求值-打印循环。
def main():
args = parse_args()
sys.path.insert(0, "")
interactive = True
load_files = []
if args.load:
for filename in args.filenames:
load_files.append(filename)
the_global_env = setup_environment()
read_eval_print_loop(env=the_global_env, startup=True,
interactive=interactive, load_files=load_files,
print_ast=args.ast)
下面是一个交互过程实例:
scm> (define (make-withdraw balance)
(lambda (amount)
(If (>= balance amount)
(begin (set! balance (- balance amount))
balance)
"Insufficient funds")))
make-withdraw
scm> (define W1 (make-withdraw 100))
w1
scm> (define W2 (make-withdraw 100))
w2
scm> (W1 50)
50
scm> (W2 70)
30
scm> (W2 40)
"Insufficient funds"
scm> (W1 40)
10
在思考求值Lisp表达式的Python程序时,有一个类比可能很有帮助。关于程序意义的一种操作式观点(operational view),就是将程序看做是一种抽象的(可能无穷大的)机器的一个描述。比如考虑下面的Lisp求阶乘程序:
(define (factorial n)
(if (= n 1)
1
(* (factorial (- n 1)) n)))
我们可以将这一程序看成一部机器的描述。这部机器包含的部分有递减(decrement)、乘法(multiply)以及相等测试(tests for equality),还有一个两位开关(即if
分支)和另一部阶乘机器(这样,阶乘机器就是无穷的,因为其中包含着另一部阶乘机器)。下图是这部阶乘机器的流程图,说明了有关的部分如何连接在一起。
按照类似的方式,我们也可以把求值器看做是一部非常特殊的机器,它要求以一部机器的描述作为输入。给定了这个输入之后,求值器就能够规划自己的行为(configures itself)来模拟被描述的机器。举例来说,如果我们将factorial
过程的定义送入求值器,求值器能够计算阶乘,如下图所示:
按照这一观点,我们的求值器可以被看做是是一种通用机器(universal machine)。它能够模拟其它的任何机器,只要它们已经被描述为Lisp程序。这是非常惊人的。比如我们为电子电路设想一种类似的求值器,这将会是一种电路,它以另一个电路(例如某个滤波器)的信号编码方案做为输入。在给了它这种输入之后,这一电路求值器能具有与这一描述所对应的滤波器同样的行为。这样的一个通用电子线路将会难以想象的复杂,而程序的求值器却是一个相当简单的程序。
事实上,任一求值器都能模拟其他的求值器。这样,有关“原则上说什么可以计算”(忽略掉有关所需时间和空间的实践性问题)的概念就与语言或计算机无关了,它反映的是一个有关可计算性(computability) 的基本概念。这一思想第一次是由图灵以清晰的方式阐述,他在1936年的论文《论可计算数及其在判定性问题上的应用》[2]为理论计算机科学奠定了基础。在这篇论文里,图灵给出了一种简单的计算模型——现在被称为图灵机——并声称,任何“有效过程”都可以描述为这种机器的一个程序(这一论断就是著名的邱奇—图灵论题)。图灵而后实现了一台通用机器,即一台图灵机,其行为就像是所有图灵机程序的求值器。
求值器的另一惊人方面,在于它就像是在我们的程序设计语言所操作的数据对象和这个程序设计语言本身之间的一座桥梁。现在设想这个用Python实现的求值程序正在运行,一个用户正在输入表达式并观察所得到的结果。从用户的观点看,他所输入的形如(* x x)
的表达式是程序设计语言里的一个表达式,是求值器将要执行的东西。而从求值器的观点看,这一表达式不过是一个表(这里就是三个符号*
、x
和x
的表),它所要做的也就是按照一套定义良好的规则去操作这个表。
这种用户程序即求值器的数据的情况,未必会成为混乱之源。事实上,有时简单地忽略这种差异,为用户提供显式地将数据对象当做Lisp表达式求值的能力,允许他们在程序里直接使用eval
,甚至可能带来许多方便。在我们实现的这个求值器里,就可以直接使用eval
过程:
scm> (eval '(* 5 5))
25
scm> (eval (cons '* (list 5 5)))
25
事实上,Python语言中也内置了eval()
函数:
>>> eval("5 * 5")
25