柯里化的前生今世(六):词法作用域和闭包

关于

本文是系列文章中的第六篇,发布在业余程序员的个人修养这个专栏中:
柯里化的前生今世(一):函数面面观
柯里化的前生今世(二):括号神教
柯里化的前生今世(三):语言和同像性
柯里化的前生今世(四):编译器与解释器
柯里化的前生今世(五):动态作用域

在上一篇中,我们介绍了动态作用域,并进行了相关名词的解释。
我们解释了什么是环境,什么是帧,如何在一个环境中对表达式求值。
我们用一个内部结构表示了函数,实现了一个微型的支持动态作用域的解释器。
这一篇,我们将再往前一步,实现词法作用域(lexical scope)。

动态作用域 vs 词法作用域

作用域(scope)的概念大家可能比较陌生,
但是闭包(closure)的概念在这几年非常流行,几乎已经没有什么主流语言不支持它了。

从实现角度,和函数一样我们将用另外一种内部结构表示闭包,

; function in the dynamic-scope
(struct function 
  (param body))

; closure in the lexical-scope
(struct closure 
  (param body env))

它封装了某函数的形参列表param,函数体body
还封装了该函数被定义时的环境env

当封装的这个函数被调用时,我们会用实参扩展封装下来的这个环境,
函数体在这个扩展后的环境中求值。
(注:在动态作用域中,我们用实参扩展的是函数被调用时的环境

对比如下,留意; here那一行

; function-call in the dynamic-scope
(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))  ; here
      (set! value (eval-exp body))
      (set! *env* env)
      value)))

; function-call in the lexical-scope
(define (eval-function-call-list exp env)  
  (display "eval-function-call-list\n")  
  (let* ((clos (eval-exp (car exp) env))         
         (arg (eval-exp (cadr exp) env))

         (body (closure-body clos))
         (lexical-env (closure-env clos))
         (param (closure-param clos))

         (frame (create-frame)))

    (extend-frame frame param arg)

    (let ((executing-env (extend-env lexical-env frame)))  ; here
      (eval-exp body executing-env))))

以上我们看到,为了能让表达式在指定的环境中求值,
我们就不能把环境看做全局变量了,我们的eval-exp要增加一个env参数。

放码过来

完整的代码如下,
我们增加了内部结构closure
修改了eval-exp让它可以接受给定的env作为参数,
同时导致了所有的相关函数都要增加env参数,包括handle-decision-tree

值得注意的是eval-function-call-list
它拿到闭包clos,会解开得到里面包装的词法环境lexical-env
然后用实参扩展它(extend-env lexical-env frame)
最后函数体在这个扩展后的环境中求值(eval-exp body executing-env)

#lang racket

; tool

(struct closure 
  (param body env))

(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 env) 
  (if (null? tree)      
      (error 'handle-decision-tree "failed to make decision")      
      (let* ((head (car tree))    
             (predicator (car head))  
             (decision (cadr head)))
        (if (predicator exp env)
            (if (not (list? decision))
                (decision exp env)
                (handle-decision-tree decision exp env))            
            (handle-decision-tree (cdr tree) exp env)))))

; env

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

; main

(define (eval-exp exp env)
  (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 env))

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

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

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

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

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

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

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

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

(define (eval-function-call-list exp env)  
  (display "eval-function-call-list\n")  
  (let* ((clos (eval-exp (car exp) env))         
         (arg (eval-exp (cadr exp) env))

         (body (closure-body clos))
         (lexical-env (closure-env clos))
         (param (closure-param clos))

         (frame (create-frame)))

    (extend-frame frame param arg)

    (let ((executing-env (extend-env lexical-env frame)))
      (eval-exp body executing-env))))

测试

(display (eval-exp '1 *env*))

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

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

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

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

词法作用域和闭包

词法作用域

我们看到测试用例中,与动态作用域不同的结果。

; dynamic-scope
(display (eval-exp '((lambda (x)
                       ((lambda (f)
                          ((lambda (x)
                             (f 3))
                           2))
                        (lambda (z) x)))
                     1)))
; result: 2

; lexical-scope
(display (eval-exp '((lambda (x)
                       ((lambda (f)
                          ((lambda (x)
                             (f 3))
                           2))
                        (lambda (z) x)))
                     1)
                   *env*))
; result: 1

当函数f被调用时(f 3),形参z => 3
但是函数体中的x,还是(lambda (z) x)函数定义时x的值x => 1
而不是(f 3)函数调用时x的值x => 2
这种性质就称为词法作用域。

闭包

结合以上的实现,我们看到,闭包只不过是一个数据结构,
它封装了函数的形参,函数体,以及该函数被定义时的环境。
并没有什么特别神秘的地方。

但是闭包的引入,方便了我们去理解程序,
让我们更容易确认函数体中自由变量的值,例如x
也让类库更安全,类库的表现和具体使用场景无关。

闭包与对象

对象

闭包与对象有很强的联系,很多面向对象编程思想的拥趸们,
经常以“对象”可以封装状态而引以为豪。
而事实上,无论是面向对象还是函数式,只不过表达思想的不同方式罢了,
词法作用域和闭包一样可以封装“状态”。

; let over lambda
(define obj
    (let ((state 1))
      (list
       (lambda () state)
       (lambda (v) (set! state v)))))

(define obj-get-state (car obj))
(define obj-set-state (cadr obj))

; test
(obj-get-state)  ; 1
(obj-set-state 2)
(obj-get-state)  ; 2

以上我们定义了一个对象obj,并得到了它的getset函数,
我们用obj-set-state函数修改状态,用obj-get-state得到内部封装的状态,
发现obj确实封装了状态,从面向对象的意义来看它就是一个“对象”了。

那么面向对象中的类是什么呢?
它其实是一个对象的工厂函数,每次new都创建一个具有独立状态的新对象。
下面我们用闭包模拟一下。

; lambda over let over lambda
(define create-obj
    (lambda ()
      (let ((state 1))
        (list
         (lambda () state)
         (lambda (v) (set! state v))))))

(define obj1 (create-obj))
(define obj1-get-state (car obj1))
(define obj1-set-state (cadr obj1))

(define obj2 (create-obj))
(define obj2-get-state (car obj2))
(define obj2-set-state (cadr obj2))

; test
(obj1-get-state)  ; 1
(obj1-set-state 2)
(obj1-get-state)  ; 2

(obj2-get-state)  ; 1
(obj2-set-state 3)
(obj2-get-state)  ; 3

我们发现,obj1obj2独立维护了自身的状态,
而且它们是用同一个工厂create-obj创建出来的,
那么这个工厂函数,就可以类比面向对象中的“类”了。

类的静态变量

在面向对象语言中,某个class是可以有静态变量的,
同一个class的这些变量值是相同的。
那么,我们在create-obj这个工厂函数之上再加一层闭包let好了,
让各个obj共享“类”的变量。

; let over lambda over let over lambda
(define let-create-obj
    (let ((hidden 5))
      (lambda ()
        (let ((state 1))
          (list
           (lambda () (+ hidden state))
           (lambda (v) (set! state v)))))))

(define obj1 (let-create-obj))
(define obj1-get-state (car obj1))
(define obj1-set-state (cadr obj1))

(define obj2 (let-create-obj))
(define obj2-get-state (car obj2))
(define obj2-set-state (cadr obj2))

; test
(obj1-get-state)  ; 6
(obj1-set-state 2)
(obj1-get-state)  ; 7

(obj2-get-state)  ; 6
(obj2-set-state 3)
(obj2-get-state)  ; 8

let over lambda

我们看到了闭包的表现力,同用一种模式模拟了面向对象语言中的很多概念,
“对象”,“类”,“类的静态变量”,看起来都那么的简洁纯净,
当然,只要我们需要,我们还可以
“lambda over let over lambda over let over lambda”,等等。

因此,理解了闭包与对象的关系之后,我们就会放平自己的心态,
不会做某种语言的无脑粉,不同的思想也会尽可能多的为我们的目标服务,
当然,Lisp也不是万能的,现在看来,它只是一个“玩具”罢了。

下文

本文实现了一个简单的具有词法作用域的Lisp方言,
为了加深理解,我们还类比了闭包与对象这两个有趣的概念。
下文,我们要介绍高大上的continuation了,这又是什么?
这不过是一个“坑”,仅此而已,敬请期待吧~


参考

On Lisp
Let Over Lambda


下一篇:柯里化的前生今世(七):first-class continuation

编辑于 2018-04-05