SICP:元循环求值器(Python实现)

求值器完整实现代码我已经上传到了GitHub仓库:TinySCM,感兴趣的童鞋可以前往查看。这里顺便强烈推荐UC Berkeley的同名课程CS 61A

在这个层次结构的最底层是对象语言。对象语言只涉及特定的域,而不涉及对象语言本身(比如它们的文法规则,或其中的其体句子)。如要涉及它们,则要有一种元语言。对于语言的两个层次这一经验,所有学习外国语的人都是很熟悉的。然后,就要有一种元元语言来讨论元语言,以此类推。

——侯世达《哥德尔、埃舍尔、巴赫:集异璧之大成》

绪论

到目前为止,我们探讨的都是通过过程抽象、数据抽象以及模块化等手段来控制系统的复杂性。为了阐释这些技术,我们一直使用的是同一种编程语言。然而,随着我们所面对的问题更复杂,我们必须经常转向新的语言,以便能够有效地表述自己的想法。实际上,建立新语言是工程设计中控制复杂性的一种威力强大的工作策略,我们常常能通过采用一种新语言而处理复杂问题的能力

元语言抽象就是建立新的语言。它在工程设计的所有分支中都扮演着重要的角色,在计算机程序设计领域更是特别重要。因为这个领域中,我们不仅可以设计新的语言,还可以通过构造求值器的方式实现这些语言。对某个程序设计语言的求值器(或者解释器)也是一个过程,在应用于这个语言的一个表达式时,它能够执行求值这个表达式所要求的动作。

把这一点看做是程序设计语言中最基本的思想一点也不过分:

求值器决定了一个程序设计语言中各种表达式的意义,而它本身也不过就是另一个程序。

事实上,我们几乎可以把任何程序看做是某个语言的求值器。比如在符号代数中,一个多项式系统包含着多项式的算数规则以及底层的实现,一个符号求导系统亦包含着函数求导的规则以及底层的实现(参见我之前的博客《SICP:符号求导、集合表示和Huffman树(Python实现)》)。从这样一种观点看,处理大规模计算机系统的技术,与构造新的程序设计语言的技术有紧密的联系,而计算机科学中一个重要的思想就是如何去构造适当的描述语言

接下来我们将要讨论如何关于在一些语言的基础上构造新的语言。在这篇博客里,我们将用Python语言去构造一个Scheme语言的求值器。事实上求值器的实现语言无关紧要,我们也可以用Scheme语言去构造Scheme语言的求值器。用于被求值语言同样的语言写出来的求值器被称为元循环(metacircular)。

从根本上说,元循环求值器也就是我们在博客《SICP:赋值和局部状态(Python实现)》中所描述求值的环境模型的一个实现形式。回忆一下,该模型包括两个部分:

  • 在求值一个组合式(combinations)(非特殊形式的复合表达式)时,首先求值其中的子表达式(包括运算符和运算对象),然后将运算符的值(即一个过程对象)应用于运算对象的值。
  • 在将一个复合过程对象应用于一集实参时,我们将在一个新环境里求值这个过程的体。构造这一新环境的方式是用一个帧来扩充该过程对象原来的环境,帧中包含的是这个过程的形参与所应用的实参之间的绑定。

这两条规则描述了求值过程的核心部分,也就是它的基本循环。在这一循环中,表达式在环境中的求值被规约到过程对实参的应用,而这种应用又被规约到新的表达式在新的环境中的求值。如此下去,直至我们下降到符号(其值可以在环境中找到)或基本过程(它们可以直接应用)。 如下图所示:

这一求值循环实际体现为求值器里的两个关键函数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类的定义及其对应的一些方法。

4.1.1 求值器的内核

求值过程可以描述为两个函数scheme_eval()scheme_apply()之间的相互作用(interplay)。

eval

scheme_eval()的参数是一个表达式和一个环境。scheme_eval()对表达式进行分类以此展开后续的求值工作。正如我们上面所说,我们采用选取函数first_expr()rest_exprs()分别去访问表达式的首元素和其余元素,并根据表达式的首元素(也即其标签)来完成表达式类型的判定。表达式的类型可做如下分类:

基本表达式:

  • 对于自求值表达式,例如数和字符串等(其实也就是我们常说的字面值,literals),scheme_eval()直接返回这个表达式本身。
  • 对于变量,scheme_eval()必须在环境中查找变量的值。

特殊形式(special forms):

  • 这里的特殊形式包括if表达式、变量的赋值/定义、引号(')表达式、lambda表达式等、我们的处理方式是将其分派给不同的名为eval_×××()的求值函数处理。如if表达式就分派给eval_if()函数来处理。

组合式(combinations):

  • 这里的组合式也即过程的应用(application)。对于一个过程应用,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标准中没有的。

apply

scheme_apply()有两个参数,一个是过程,一个是该过程去应用的实参表。scheme_apply()将过程分为两类:基本过程(求值器内置)和复合过程(通过definelambda/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)

最后,我们还需要实现andor这两个布尔表达式:

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)

如上所示,andor表达式都是短路(short-circuited)的。对于and表达式,如果其子表达式有求值为False的,则直接返回False;对于or表达式,如果其子表达式有求值为True的,直接返回True

序列

eval_sequence()用在scheme_apply()里,用于求值过程体里的表达式序列。它也用在scheme_eval()里,用于求值beginlet表达式内的表达式序列。这个过程以一个表达式序列和一个环境为参数,按照序列里表达式出现的顺序对它们进行求值,并返回最后一个表达式的值。

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表达式的形参和过程体,并结合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内部可以嵌套多个quasiquoteunquote,故需要在递归时维护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")

4.1.2 表达式的表示

这个求值器很像我们在博客《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表达式是由符号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)))

规约为对下面涉及ifbegin的表达式的求值问题:

(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))

4.1.3 求值器数据结构

除了需要定义表达式的外部语法之外,求值器的实现还必须定义好在其内部实际操作的数据结构,做为程序执行的一部分。例如序列数据结构,过程和环境的表示形式,真和假的表示方式等等。

序列数据结构
如我们在博客《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()方法在当前对象所对应环境的第一个帧里加入一个新绑定,它关联起变量varval
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节。

4.1.4 作为程序运行这个求值器

有了一个求值器,我们手头上就有了一个有关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

4.1.5 将数据作为程序

在思考求值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)的表达式是程序设计语言里的一个表达式,是求值器将要执行的东西。而从求值器的观点看,这一表达式不过是一个表(这里就是三个符号*xx的表),它所要做的也就是按照一套定义良好的规则去操作这个表。

这种用户程序即求值器的数据的情况,未必会成为混乱之源。事实上,有时简单地忽略这种差异,为用户提供显式地将数据对象当做Lisp表达式求值的能力,允许他们在程序里直接使用eval,甚至可能带来许多方便。在我们实现的这个求值器里,就可以直接使用eval过程:

scm> (eval '(* 5 5))
25
scm> (eval (cons '* (list 5 5)))
25

事实上,Python语言中也内置了eval()函数:

>>> eval("5 * 5")
25

参考

  • [1] Abelson H, Sussman G J. Structure and interpretation of computer programs[M]. The MIT Press, 1996.
  • [2] Turing A M. On computable numbers, with an application to the Entscheidungsproblem[J]. J. of Math, 1936, 58(345-363): 5.
posted @ 2023-05-15 20:14  orion-orion  阅读(514)  评论(0编辑  收藏  举报