日拱一卒,伯克利CS61A,实现scheme解释器(三)

2022-09-21 12:30:13 浏览数 (1)

作者 | 梁唐

出品 | 公众号:Coder梁(ID:Coder_LT)

大家好,日拱一卒,我是梁唐。

我们继续来肝伯克利CS61A的scheme project,这是这个project的第三篇,如果漏掉了之前的建议先去补一下。

原始文档:https://inst.eecs.berkeley.edu/~cs61a/sp18/proj/scheme/#interpreter-details

在上一篇文章当中,我们进一步完善了scheme解释器的功能,让它能够支持自定义函数等许多功能。在这一篇文章当中,我们要继续完善功能,让我们的解释器能够支持更多语法。

Special Forms

整个项目到这里我们已经完成了基础的核心部分,接下来还要处理一些特殊的类型。

我们还有很多语法没有支持,比如逻辑运算符if,and, or, cond这些。这些表达式使用频率非常高,也非常重要。

在scheme当中,只有False才是假,所有其他的值都会被视为真,包括0和nil。你可以使用scheme_primitive.py文件中的scheme_truepscheme_falsep函数来判断一个值是true还是false

注意:scheme传统中使用#f来表示false#t表示true。在我们的解释器当中true, True, #t均视为等价。然而在解锁测试数据时,需要使用#t#f

我们提供了if语句的实现,do_if_form函数作为参考。确保你已经理解了if语句的原理,再进行下面的题目。

Problem 13

实现do_and_formdo_or_form函数来完成andor表达式的支持。

对于and语句来说,你需要从左往右evaluate所有的表达式,如果遇到结果是false,那么返回#f。否则返回最后一个子语句的结果。如果输入的语句为空,也返回#t

对于or语句来说,我们也需要从左往右评估每一个子语句的值。如果某一个子语句的结果是true,直接返回。否则返回#f。如果输入为空,也返回#f

在开发之前,先答题解锁测试:

代码语言:javascript复制
python3 ok -q 13 -u

开发之后,测试:

代码语言:javascript复制
python3 ok -q 13
答案

这两个函数逻辑并不复杂,使用递归很容易搞定。

代码语言:javascript复制
def do_and_form(expressions, env):
    """Evaluate a (short-circuited) and form."""
    # BEGIN PROBLEM 13
    "*** YOUR CODE HERE ***"
    if expressions is nil:
        return True
    elif expressions.second is nil:  # Tail context
        return scheme_eval(expressions.first, env)
    else:
        first_expr = scheme_eval(expressions.first, env)
        if scheme_falsep(first_expr):  # The first expression is False
            return False
        elif scheme_truep(first_expr):  # The first expression is True
            return do_and_form(expressions.second, env)
    # END PROBLEM 13

def do_or_form(expressions, env):
    """Evaluate a (short-circuited) or form."""
    # BEGIN PROBLEM 13
    "*** YOUR CODE HERE ***"
    if expressions is nil:
        return False
    elif expressions.second is nil:  # Tail context
        return scheme_eval(expressions.first, env)
    else:
        first_expr = scheme_eval(expressions.first, env)
        if scheme_falsep(first_expr):  # The first expression is False
            return do_or_form(expressions.second, env)
        else:  # The first expression is True
            return first_expr
    # END PROBLEM 13

Problem 14

完善do_cond_form函数,让他能够返回第一个为true的子语句的值,如果都为false,返回else语句的值。

然而存在一些特殊情况:

  • 当判断为true的值没有对应的返回结果,那么返回该值
  • cond语句的某一个分支中存在多个结果语句时,返回最后一个,提示,可以使用eval_all函数

你的代码需要能通过下列测试数据:

如果cond语句既没有为true的分支也没有else语句,那么返回None

编码之前,先回答问题解锁测试:

代码语言:javascript复制
python3 ok -q 14 -u

编码之后,进行测试:

代码语言:javascript复制
python3 ok -q 14
答案

整个函数的框架已经搭好了,函数内部使用了while循环来遍历所有的表达式,并且实现了else语句的部分。

test表示当前的语句是否是true,如果是真,那么返回test之后的内容。但由于题目中说了,有些情况只有判断条件,没有返回结果。这个时候就要返回判断条件的结果本身,也就是test。当然也有可能表达式有多个,这种情况使用eval_all函数返回最后一个表达式的结果即可。

如果test为假则继续往下执行其他条件。

代码语言:javascript复制
def do_cond_form(expressions, env):
    """Evaluate a cond form."""
    while expressions is not nil:
        clause = expressions.first
        check_form(clause, 1)
        if clause.first == 'else':
            test = True
            if expressions.second != nil:
                raise SchemeError('else must be last')
        else:
            test = scheme_eval(clause.first, env)
        if scheme_truep(test):
            # BEGIN PROBLEM 14
            "*** YOUR CODE HERE ***"
            if clause.second == nil:
                return test
            return eval_all(clause.second, env)
            # END PROBLEM 14
        expressions = expressions.second

Problem 15

let特殊类型,它会在本地绑定symbol和value,但是这个绑定只会在原地生效,比如这个例子:

(let ((x 42) (y (* x 10))) (list x y))语句当中,绑定y时用到的x是全局的值,也就是5,而不是刚刚绑定的42。因为绑定值的操作是最后一起执行的,xy是一起绑定的。有一点类似于以下的Python代码:

代码语言:javascript复制
x = 5
x, y = 42, x * 10

实现make_let_frame方法,它会返回一个当前env的子frame,在这个frame当中,将symbol绑定到对应的value上。bindings scheme list包含一系列pairs,其中每一个包含一个symbol和对应的表达式。

你可能会用到下面这些函数:

  • check_form:这个函数用来检查binding结构
  • check_formals:这个函数检查formal参数,确保每一个symbol都是不同的
  • make_child_frame:这个函数(你在11题中开发的),它接收一个Pair表示formal,一个Pair表示value。返回一个绑定了formal和value的新frame

在你编码之前,先回答问题解锁测试样例:

代码语言:javascript复制
python3 ok -q 15 -u

编码之后,进行测试:

代码语言:javascript复制
python3 ok -q 15
答案

只要搞清楚了let语句是先评估,评估完了之后一起绑定的,这题就算是搞定了一大半。

但坑爹的是,原文的题意当中没有明确地指出这一点,这是要我们基于老师给的问题和一些描述去自己分析和推理的。所以我当时做这题的时候,还是费了一些周折。

代码语言:javascript复制
def make_let_frame(bindings, env):
    """Create a child frame of ENV that contains the definitions given in
    BINDINGS. The Scheme list BINDINGS must have the form of a proper bindings
    list in a let expression: each item must be a list containing a symbol
    and a Scheme expression."""
    if not scheme_listp(bindings):
        raise SchemeError('bad bindings list in let form')
    # BEGIN PROBLEM 15
    "*** YOUR CODE HERE ***"
    formals, args = nil, nil
    while bindings is not nil:
        check_form(bindings.first, 2, 2)
        formals = Pair(bindings.first.first, formals)
        args = Pair(scheme_eval(bindings.first.second.first, env), args)
        bindings = bindings.second
    check_formals(formals)
    return env.make_child_frame(formals, args)
    # END PROBLEM 15

Problem 16

实现do_mu_form函数来评估特殊类型mumu不是一种标准的scheme表达式类型。

mu表达式类似于lambda表达式,但不同的是,当evaluate mu语句时,它是动态scope的。lambda表达式是静态scope,因为lambda表达式执行的时候,使用的envlambda创建时的frame。而mu使用的是表达式执行时的frame,因此是动态的。

mu表达式通过MuProcedure类实现,大部分代码已经开发好了。

完善MuProcedure类,当它被调用的时候,它依赖的外部frame是动态的,也正因此,MuProcedure类中不需要存储frame实例。

下面是一个mu表达式的例子:

编码之前,先回答问题确保理解正确:

代码语言:javascript复制
python3 ok -q 16 -u

编码完成之后,进行测试:

代码语言:javascript复制
python3 ok -q 16
答案

我们看下mu表达式的使用方法,其实它和lambda的语法是一样的。所以do_mu_formdo_lambda_form的逻辑也是一样的。

代码语言:javascript复制
def do_mu_form(expressions, env):
    """Evaluate a mu form."""
    check_form(expressions, 2)
    formals = expressions.first
    check_formals(formals)
    # BEGIN PROBLEM 16
    "*** YOUR CODE HERE ***"
    return MuProcedure(formals, expressions.second)
    # END PROBLEM 16

唯一的区别就是运行时的envlambda是静态的,而mu是动态的。

看起来好像很复杂,但仔细思考会发现这两个在实现里的区别其实很小。只有创建子frame的时候有一些区别,lambda表达式创建运行时的frame用的是自身实例中存储的env,这样就能保证不论什么时候运行lambda,使用的都是创建时的frame

mu是动态的,从类的定义中我们也可以看出来,它没有env这个实例属性。那么我们在创建子frame的时候用的就不是本身存储的env,而是外界传入的。外界传入的env是在运行时传入的,这样就可以保证了,mu运行时依赖的外部变量是动态的。

代码只有一行,但理清楚这里面的逻辑却不简单。

代码语言:javascript复制
class MuProcedure(Procedure):
    """A procedure defined by a mu expression, which has dynamic scope.
     _________________
    < Scheme is cool! >
     -----------------
               ^__^
               (oo)_______
                (__)       )/
                    ||----w |
                    ||     ||
    """

    def __init__(self, formals, body):
        """A procedure with formal parameter list FORMALS (a Scheme list) and
        Scheme list BODY as its definition."""
        self.formals = formals
        self.body = body

    # BEGIN PROBLEM 16
    "*** YOUR CODE HERE ***"
    def make_call_frame(self, args, env):
        return env.make_child_frame(self.formals, args)
    # END PROBLEM 16

    def __str__(self):
        return str(Pair('mu', Pair(self.formals, self.body)))

    def __repr__(self):
        return 'MuProcedure({0}, {1})'.format(
            repr(self.formals), repr(self.body))

到这里,整个scheme的解释器就算是完成了。

但解释器除了要支持解释执行scheme代码之外,其实还有很多其他的内容。在附加题当中,老师给我们留了两个非常困难的问题:尾递归和尾递归的运行优化。

这部分内容比较困难,而且涉及到更多的知识,因此我们单独成篇,放在之后的文章当中和大家分享。

喜欢本文的话不要忘记三连~

0 人点赞