[PLT] 柯里化的前生今世(五):动态作用域

关于

本文是系列文章中的第五篇,
上一篇中,我们介绍了编译器和解释器,抽象语法树与S表达式的关系,并且我们还打算写一个极简的元循环解释器。通过写这个解释器,一方面我们可以熟悉Racket语言,另一方面,可以帮助我们从实现角度来理解某些高级概念。

概览

(废话少说言归正传,一言不合就贴代码。。
这段代码我们用Racket实现了一个具有动态作用域Lisp方言的解释器。
我们没有使用Racket的模式匹配match,而是遵循一般教学用的常规写法,
把S表达式分为3种:符号,自求值表达式,列表(函数定义,函数调用)。

如果是符号,我们就得去“环境”中查找符号的值。
如果是自求值表达式,我们就直接返回它。(我们这里的自求值表达式只有数字
如果是函数定义,我们就用一个数据结构把参数和函数体存起来。
如果是函数调用,我们就先用实参“扩展”调用时的“环境”,然后让函数体在这个环境中求值。

#lang racket

; tool

(struct function 
  (param body))

(define (create-frame)
  (make-hash))

(define (extend-frame frame key value)
  (hash-set! frame key value))

(define (extend-env env frame)
  (cons frame env))

(define (get-symbol-value env key)
  (let lookup-env
    ((env env))
    (if (null? env)
        (error 'get-symbol-value "failed to find symbol")
        (let ((head-frame (car env)))
          (if (hash-has-key? head-frame key)
              (hash-ref head-frame key '())
              (lookup-env (cdr env)))))))

(define (handle-decision-tree tree exp)
  (if (null? tree)
      (error 'handle-decision-tree "failed to make decision")
      (let* ((head (car tree))
             (predicator (car head))
             (decision (cadr head)))
        (if (predicator exp)
            (if (not (list? decision))
                (decision exp)
                (handle-decision-tree decision exp))
            (handle-decision-tree (cdr tree) exp)))))

; env

(define *env* `(,(create-frame)))

; main

(define (eval-exp exp)
  (handle-decision-tree 
   `((,is-symbol? ,eval-symbol)
     (,is-self-eval-exp? ,eval-self-eval-exp)
     (,is-list?
      ((,is-lambda? ,eval-lambda)
       (,is-function-call-list? ,eval-function-call-list))))
   exp))

(define (is-symbol? exp)
  (display "is-symbol?\n")
  (symbol? exp))

(define (eval-symbol exp)
  (display "eval-symbol\n")
  (get-symbol-value *env* exp))

(define (is-self-eval-exp? exp)
  (display "is-self-eval-exp?\n")
  (number? exp))

(define (eval-self-eval-exp exp)
  (display "eval-self-eval-exp\n")
  exp)

(define (is-list? exp)
  (display "is-list?\n")
  (list? exp))

(define (is-lambda? exp)
  (display "is-lambda?\n")
  (eq? (car exp) 'lambda))

(define (eval-lambda exp)
  (display "eval-lambda\n")
  (let ((param (caadr exp))
        (body (caddr exp)))
    (function param body)))

(define (is-function-call-list? exp)
  (display "is-function-call-list?\n")
  #t)

(define (eval-function-call-list exp)
  (display "eval-function-call-list\n")
  (let* ((fn (eval-exp (car exp)))
         (arg (eval-exp (cadr exp)))
         
         (body (function-body fn))
         (param (function-param fn))
         
         (frame (create-frame)))
    
    (extend-frame frame param arg)
    
    (let* ((env *env*)
           (value '()))
      (set! *env* (extend-env *env* frame))
      (set! value (eval-exp body))
      (set! *env* env)
      value)))

测试

(display (eval-exp '1))

(display "\n\n")
(display (eval-exp '(lambda (x) x)))

(display "\n\n")
(display (eval-exp '((lambda (x) x) 
                     1)))

(display "\n\n")
(display (eval-exp '((lambda (x)
                       ((lambda (y) x)
                        2))
                     1)))

(display "\n\n")
(display (eval-exp '((lambda (x)
                       ((lambda (f)
                          ((lambda (x)
                             (f 3))
                           2))
                        (lambda (z) x)))
                     1)))

名词解释

今天这篇文章中提到了很多“耳熟能详”的概念名词,下面通过代码来解释它们。

环境

(define *env* `(,(create-frame)))

环境是帧(frame)的列表,其中“ ` ”是反引用(quasi-quotation),
反引用是一种模板,便于构建列表。

`(,(create-frame))
<=> (list (create-frame))

(create-frame)创建了一个帧,然后把它放到列表中,该列表目前只有一个元素。
这个帧的列表就是环境。

我们这里定义了一个全局变量*env*
任何一个函数调用和返回,都会影响*env*

帧(frame)

帧是键值对的集合,表示了符号与值的映射关系,我们称之为绑定(binding)。

(define (create-frame)
  (make-hash))

(define (extend-frame frame key value)
  (hash-set! frame key value))

具体实现中,我们使用了哈希表来存储键值对。
帧是可以被扩展的,extend-frame函数用新的键值对扩展了已有的帧。

环境的扩展

(define (extend-env env frame)
  (cons frame env))

我们知道了,环境是帧的列表,环境还可以用新的帧来进行扩展。
这样环境中就可以包含很多个帧了,新的帧会放在列表头部。

求值

对一个符号进行求值,就是去环境中查找该符号所对应的值。
该符号与值的映射关系,可能体现在环境中的不同帧中,
我们取列表头部开始第一个映射关系。
即,我们认为后加入的帧,覆盖(shadow)了以前的绑定(binding)关系。

具体实现如下,我们递归的使用lookup-env进行查找。

(define (get-symbol-value env key)
  (let lookup-env
    ((env env))
    (if (null? env)
        (error 'get-symbol-value "failed to find symbol")
        (let ((head-frame (car env)))
          (if (hash-has-key? head-frame key)
              (hash-ref head-frame key '())
              (lookup-env (cdr env)))))))

函数

函数是一个数据结构,

(struct function 
  (param body))

该结构的每个实例都表示一个具体的函数,
其中包含两个字段,param表示参数列表,body表示函数体。

当我们遇到函数定义表达式时,我们这样定义了一个函数(数据结构)。

(define (eval-lambda exp)
  (display "eval-lambda\n")
  (let ((param (caadr exp))
        (body (caddr exp)))
    (function param body)))

当我们求值一个函数调用表达式时,
(1)我们先把parambody字段提取出来
(2)创建一个新的帧,其中保存了形参到值的映射关系
(3)用新的帧扩展全局环境
(4)在全局环境中求值函数体
(5)函数返回后,恢复原来的环境

我们看到,这里有一个帧的添加和删除操作,
如果函数调用中还有另一个函数调用,就会在帧删除之前又添加一个帧。
实际上,这是一个入栈弹栈操作。

因此,我们说环境的具体实现形式是列表,但在数据结构上,它构成了一个栈。
帧在这种情况下,也成为栈帧(stack frame)。

(define (eval-function-call-list exp)
  (display "eval-function-call-list\n")
  (let* ((fn (eval-exp (car exp)))
         (arg (eval-exp (cadr exp)))
         
         (body (function-body fn))
         (param (function-param fn))
         
         (frame (create-frame)))
    
    (extend-frame frame param arg)
    
    (let* ((env *env*)
           (value '()))
      (set! *env* (extend-env *env* frame))
      (set! value (eval-exp body))
      (set! *env* env)
      value)))

求值过程

现在我们对相关概念都进行了解释,我们来看看整个求值过程吧。

当我们遇到(eval-exp '1)时,会发现1是一个数字,我们断定为它是一个自求值表达式,直接返回它,因此(eval-exp '1)的值就是1了。

当我们遇到(eval-exp '(lambda (x) x))时,会发现这是一个函数定义表达式,我们拿到它所定义函数的参数列表param => (x)和函数体body => x,创建了一个结构——function,来存储它们。

当我们遇到(display (eval-exp '((lambda (x) x) 1))),我们发现这是一个函数调用表达式(实际上,如果不是其他类型的表达式,我们都认为是函数调用),首先得到函数对象,然后再得到函数对象中保存的参数列表和函数体,在扩展中环境中求值函数体,因为这时环境中x => 1,所以body => x => 1,结果为1

同理,我们可以分析其他的测试用例。

动态作用域

我们来看最后一个测试用例,

(eval-exp '((lambda (x)
              ((lambda (f)
                 ((lambda (x)
                    (f 3))
                  2))
               (lambda (z) x)))
            1))

首先x => 1,然后f => (lambda (z) x)
f被调用(f 3)之前,我们覆盖了x的值,x => 2
这时候,求值f的函数体,得到了(f 3) => 2

我们看到f => (lambda (z) x)函数体中的x
是在f被调用的环境中求值的,f在不同的位置被调用,x可能不同。

((lambda (x)
   (f 3))
 ?)

我们可以给x任意的值。

这样有利有弊,
因为实现起来简单,很多古老的语言都是动态作用域的,
可是会带来上面所说的不确定性,类库的设计者无法预测所有被调用的场景,
很容易出现问题。

后来从Scheme开始,人们提出了词法作用域(静态作用域)的概念,
f => (lambda (z) x)中的x始终等于f被定义处的x => 1
这就避免了上述问题,当我们想确定x的值时,
只需要去f被定义的地方找一下,看看代码就行了。
(通过看看代码,分析文本,因此称为“词法的”。。

下文

本文我们实现了一个简单的具有动态作用域的Lisp方言,
下文,我们将在它的基础上进行修改,让它支持词法作用域,
我们会引入词法环境和闭包的概念,请拭目以待吧。。

参考

The Racket Reference
SICP : 4.1元循环求值器
An Introduction to Scheme and its Implementation

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,290评论 6 491
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,107评论 2 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,872评论 0 347
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,415评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,453评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,784评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,927评论 3 406
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,691评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,137评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,472评论 2 326
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,622评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,289评论 4 329
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,887评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,741评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,977评论 1 265
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,316评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,490评论 2 348

推荐阅读更多精彩内容