CPS变换

玩弄Lisp系列第一弹:从王垠的40行CPS变换说起  

2013-12-20 18:00:37|  分类: 默认分类 |  标签:lisp   |举报 |字号 订阅

 
为什么要写这个?

Continuation对于每一个lisper来说都不陌生,CPS相对麻烦一点,而CPS变换可能就相当麻烦了.
我 第一次接触Continuation是在Paul Graham的On Lisp一书中,当时看到那种神奇的能力真的是不明觉厉,后来我开始学习Scheme,我发现Scheme真的是一个比Common Lisp更值得学习的语言,我发现我在学习Scheme过程中取得的进步是学习Common Lisp时所不能比的,尤其是在学习EOPL一书过程中,我用多种方式实现了Continuation,对其有了近乎本质性的理解.所以现在回过头来看看 On Lisp中那坑爹的"Continuation",我只能觉得幼稚,看着那坑爹的"bind=",我只能说"这也算Continuation?"

第一次看到王垠的CPS变换时,我还不怎么了解Continuation,所以那代码对我来说就是天书.但现在情况已经不同了,再次看到这代码时,我能清楚地感觉到自己的进步,因此写一下这个作为我学习Lisp一年三个月的总决.

正文
这里我假设读者已经知道Continuation和CPS是怎么回事,不明白的可以先去看EOPL(Essentials of Programming Languages).

这里我将使用Racket实现CPS变换,因此很多细节都会和王垠的代码不同,但基本思想是一样的.

首先来看看这个cps函数

(define cps
  (lambda (sexp ctx)
    ...))

这里的sexp就是要处理的输入,我们先严格地定义一下

sexp ::= self-evaluate       ;例如数字,布尔值
          |  symbol(不能是k)     ;符号,也可以叫变量名
          |  (quote anything)    ;引用形式,大家因该很熟悉
          |  (if sexp sexp sexp) ;if语句
          |  (lambda (arg) sexp) ;lambda表达式
          |  (sexp sexp)         ;过程的调用

self-evaluate ::= number | boolean

我们把cps函数的输出叫做cps-exp,对CPS有了解的话我们不难发现对应sexp的cps-exp应该是长这个样子的

cps-exp ::= simple-exp
             | (k simple-exp)
             | (simple-exp simple-exp k-exp)
             | (if simple-exp cps-exp cps-exp)

k-exp ::= (lambda (var) cps-exp)

simple-exp ::= self-evaluate | symbol | (quote anything)
                     |  (lambda (arg k) cps-exp)

这样一来cps函数就是要实现(sexp -> cps-exp)这样的变换,
而那个ctx参数的类型则是(simple-exp -> cps-exp).
ctx 代表了evaluation context,例如在(list (a b) 1)中, (a b)的evalution context就是(list [] 1), []在这里就表示(a b)的结果要填入的洞,写成cps就是(a b (lambda (v) (list v 1))).
所以continuation是evaluation context的一种表示方式.不过在这里ctx不能当作continuation(所以也不叫作k),因为它并不总是处于尾调用的位置.
弄 清楚这个ctx是做什么的,想出要用到这个ctx就是算法中在最难的一步(尽管不用也可以,就像cl-cont那样,但是想要生成最简的结果就比较麻烦 了,cl-cont生成的代码冗余度非常大,不过在现代Common Lisp编译器的优化能力下似乎没有什么影响),但既然我们现在已经弄清楚这些东西的类型,那么要实现这个变换已经是手到擒来的了.

首先考虑前面三种情况

sexp ::= self-evaluate       ;例如数字,布尔值
          |  symbol                 ;符号,也可以叫变量名
          | (quote anything)  ;引用形式,大家因该很熟悉

注意到
cps-exp ::= simple-exp
simple-exp ::= self-evaluate | symbol | (quote anything)

这个sexp直接对应simple-exp,
实际上我们不用做任何事,但是要记清楚simple-exp不能直接返回,因为(simple-exp -> cps-exp)由ctx完成,所以我们把sexp传递给ctx.

(define cps
  (lambda (sexp ctx)
    (match sexp
      [(? self-evaluate?) (ctx sexp)]
      [(? symbol?) (ctx sexp)]
      [`(quote ,thing) (ctx sexp)])))

大家应该看出来了如果我们要(cps 1 _) => 1,那我们要在 _ 填入的就是identity函数,即
(define id (lambda (v) v))



下面开始考虑过程的调用

sexp ::= (sexp sexp)

对应的cps-exp是

cps-exp ::= (simple-exp simple-exp k-exp)

k-exp ::= (lambda (var) cps-exp)

simple-exp ::= self-evaluate | symbol | (quote anything)

那么对应的代码应该是长这样子的:
[`(,rator ,rand) ...]
因为rator和rand都是sexp,因此都必须递归调用cps函数对他们进行处理,而ctx参数就对应简化后的simple-exp
[`(,rator ,rand) (cps rator
                      (lambda (simple-rator)
                        (cps rand
                                (lambda (simple-rand)
                                  ...))))]
由于
cps-exp ::= (simple-exp simple-exp k-exp)
结果很自然就是
[`(,rator ,rand) (cps rator
                                   (lambda (simple-rator)
                                      (cps rand
                                              (lambda (simple-rand)
                                                 `(,simple-rator ,simple-rand ...)))))]
我们再来看看这个...中的k-exp是什么东西,由于
k-exp ::= (lambda (var) cps-exp)
所以
[`(,rator ,rand) (cps rator
               
                    (lambda (simple-rator)
       
                                (cps rand
                     
                          (lambda (simple-rand)
                      
                           `(,simple-rator ,simple-rand
                                  
                    (lambda (var) ...))))))]
我们来看一下, ...中应该填入一个cps-exp,而rator,rand已经进行过变换了,没有需要做cps变换的部分的,不能用cps函数得到cps-exp了,剩下的能产生cps-exp的就只有ctx了,那ctx的参数又是什么呢
看看这个例子,
(a (b c)) -> (b c (lambda (var) (a var (lambda (var) var))))
很明显我们不应该再做多余的事,ctx的参数就是'var,多余的事就交给ctx来完成.
[`(,rator ,rand) (cps rator
                 
                  (lambda (simple-rator)
       
                                (cps rand
                     
                          (lambda (simple-rand)
                              
                   `(,simple-rator ,simple-rand
                          
                            (lambda (var) ,(ctx 'var)))))))]
但是注意!如果就这样结束,我们在处理((a b) (c d))这样的式子时,将会出现(a b (lambda (var) (c d (lambda (var) (var var)))))这样的结果,这肯定是有问题的.
我们可以利用gensym来生成不同的名字.
[`(,rator ,rand) (cps rator
                
                   (lambda (simple-rator)
         
                              (cps rand
                      
                         (lambda (simple-rand)
                      
                            (let ([var (gensym)])
                           
               `(,simple-rator ,simple-rand
                                    (lambda (,var) ,(ctx var))))))))]


下面加入lambda表达式
sexp ::= (lambda (arg) sexp)

对应的cps-exp是
cps-exp ::= simple-exp
        | (k simple-exp)
        | (simple-exp simple-exp k-exp)

simple-exp ::= (lambda (arg k) cps-exp)
所以
[`(lambda (,arg) ,sexp) ...]
外面这层(lambda (arg k) ...)是一个simple-exp,所以把它交给ctx
[`(lambda (,arg) ,sexpe)
       (ctx `(lambda (,arg k) ...))]
而(lambda (arg) sexp)中的sexp必须用cps函数递归地变换
[`(lambda (,arg) ,sexp)
       (ctx `(lambda (,arg k) ,(cps sexp ...)))]
我们再来看几个例子
((lambda (a) a) 1) => ((lambda (a k) (k a)) 1)

(lambda (a) (f (g a))) => (lambda (a k) (g a (lambda (v) (f v (lambda (v) (k v))))))
lambda表达式里面的过程不管多么复杂,最后的结果都要用k来返回,所以
[`(lambda (,arg) ,sexp)
       (ctx `(lambda (,arg k) ,(cps sexp (lambda (v) `(k ,v)))))]
而(k simple-exp)也正是CPS程序中经常看到的语句
我们可以把这个ctx记下来,叫做ctx0
(define ctx0
  (lambda (v)
    `(k ,v)))
最后
[`(lambda (,arg) ,sexp)
       (ctx `(lambda (,arg k) ,(cps sexp ctx0)))]


下面我们再加入if表达式
[`(if ,test ,then ,else) ...]
注意到
sexp ::= (if sexp sexp sexp)

cps-exp ::= (if simple-exp cps-exp cps-exp)
经过前面的尝试,现在这个变换关系已经很明显了
[`(if ,test ,then ,else) (cps test 
                              (lambda (simple-test)
                                `(if ,simple-test
                                     ,(cps then ctx)
                                     ,(cps else ctx))))]

至此,这个程序的骨架已经完成,我们把他们组装起来.
#lang racket

(define self-evaluate?
  (lambda (thing)
    (or (number? thing) (boolean? thing))))

(define id
  (lambda (v) v))

(define ctx0
  (lambda (v)
    `(k ,v)))

(define cps
  (lambda (sexp ctx)
    (match sexp
      [(? self-evaluate?) (ctx sexp)]
      [(? symbol?) (ctx sexp)]
      [`(quote ,anything) (ctx sexp)]
      [`(if ,test ,then ,else) (cps test 
                              (lambda (simple-test)
                                `(if ,simple-test
                                     ,(cps then ctx)
                                     ,(cps else ctx))))]
      [`(lambda (,arg) ,sexp) (ctx `(lambda (,arg k) ,(cps sexp ctx0)))]
      [`(,rator ,rand) (cps rator
                      (lambda (simple-rator)
                        (cps rand
                             (lambda (simple-rand)
                               (let ([var (gensym)])
                                 `(,simple-rator ,simple-rand
                                    (lambda (,var) ,(ctx var))))))))])))



下面我们可以对其进行改进,以便于生成更简洁易读的结果

首先我们看
> (cps '(lambda (v) (f a)) id)
'(lambda (v k) (f a (lambda (var) (k var))))

很明显,这个(lambda (var) (k var))可以做一个beta-归约变成k,此时ctx是ctx0,
所以
[`(,rator ,rand) (cps rator
                      (lambda (simple-rator)
                        (cps rand
                             (lambda (simple-rand)
                               (if (eq? ctx ctx0)
                                   `(,simple-rator ,simple-rand k)
                                   (let ([var (gensym)])
                                     `(,simple-rator ,simple-rand
                                        (lambda (,var) ,(ctx var)))))))))]


下面开始加入一些原语,也就是接受单一参数,不接受k参数的过程,像zero?这样
增加定义
simple-exp |= (primitive-rator simple-exp)
先定义一些这样的过程
(define primitive-rator?
  (lambda (x)
    (memq x '(add1 sub1 zero? car cdr))))

这样(car v) => (car v), (f (car x)) => (f (car x) (lambda (var) var)),不需要k,
我们要在[`(,rator ,rand) ...]里处理这种情况
显然最终结果`(,simple-rator ,simple-rand)是一个simple-exp,对于一个simple-exp我们只需直接交给ctx

 [`(,rator ,rand) (cps rator
                       (lambda (simple-rator)
                         (cps rand
                              (lambda (simple-rand)
                                (cond [(primitive-rator? simple-rator)     
                                       (ctx `(,simple-rator ,simple-rand))] ;就是这样
                                      [(eq? ctx ctx0)
                                       `(,simple-rator ,simple-rand k)]
                                      [else (let ([var (gensym)])
                                              `(,simple-rator ,simple-rand
                                                 (lambda (,var) ,(ctx var))))])))))]


同理,如果要再加入一些二元操作符作为原语
[`(,op ,a ,b) (cps a
                   (lambda (a)
                     (cps b
                          (lambda (b)
                            (ctx `(,op ,a ,b))))))]

还有一个情况就是
> (cps '(f (if a b c)) id)
'(if a (f b (lambda (v61460) (v61460)) (f c (lambda (v61461) v61461))))

注意到这里的(f _ (lambda (var) var))出现了两次,为了减少重复,我们可以把它变成

(let ([k (lambda (v) (f v (lambda (v) v)))]) 
  (if a (k b) (k c)))

追加定义

cps-exp |= (let ([k k-exp])
             cps-exp)

不过当ctx是ctx0时,结果会
(let ((k (lambda (v) (k v))))
  (if a (k b) (k c)))
没有必要

当ctx是id时
(let ((k (lambda (v) v))) (if a (k b) (k c)))
也没有必要

所以最终结果就是
[`(if ,test ,then ,else) (cps test 
                              (lambda (simple-test)
                                (if (memq ctx `(,ctx0 ,id))
                                    `(if ,simple-test
                                         ,(cps then ctx)
                                         ,(cps else ctx))
                                    `(let ([k (lambda (v) ,(ctx 'v))])
                                          (if ,simple-test
                                              ,(cps then ctx0)
                                              ,(cps else ctx0))))))]

这样基本上就等同于王垠的算法了.如果再弄个大过程包装起来,就一样了.

最终得到的代码就是

#lang racket

(define self-evaluate?
  (lambda (thing)
    (or (number? thing) (boolean? thing))))

(define id
  (lambda (v) v))

(define ctx0
  (lambda (v)
    `(k ,v)))

(define primitive-rator?
  (lambda (x)
    (memq x '(add1 sub1 zero? car cdr))))

(define cps
  (lambda (sexp ctx)
    (match sexp
      [(? self-evaluate?) (ctx sexp)]
      [(? symbol?) (ctx sexp)]
      [`(quote ,thing) (ctx sexp)]
      [`(if ,test ,then ,else) (cps test 
                                    (lambda (simple-test)
                                      (if (memq ctx `(,ctx0 ,id))
                                          `(if ,simple-test
                                               ,(cps then ctx)
                                               ,(cps else ctx))
                                          `(let ([k (lambda (v) ,(ctx 'v))])
                                             (if ,simple-test
                                                 ,(cps then ctx0)
                                                 ,(cps else ctx0))))))]
      [`(lambda (,arg) ,sexp) 
       (ctx `(lambda (,arg k) ,(cps sexp ctx0)))]
      [`(,rator ,rand) (cps rator
                            (lambda (simple-rator)
                              (cps rand
                                   (lambda (simple-rand)
                                     (cond [(primitive-rator? simple-rator)     
                                            (ctx `(,simple-rator ,simple-rand))]
                                           [(eq? ctx ctx0)
                                            `(,simple-rator ,simple-rand k)]
                                           [else (let ([var (gensym)])
                                                   `(,simple-rator ,simple-rand
                                                      (lambda (,var) ,(ctx var))))])))))]
      [`(,op ,a ,b) (cps a
                         (lambda (a)
                           (cps b
                                (lambda (b)
                                  (ctx `(,op ,a ,b))))))])))

我们掌握了这个ctx的用法之后,可以把它应用到许多的场合,例如对lambda演算的ANF变换

(define anf 
  (lambda (lc-exp)
    (define var? symbol?)
    (define val?
      (lambda (v)
        (match v
          [(? var?) #t]
          [`(lambda (,x) ,bd) #t]
          [_ #f])))
    (define id (lambda (x) x))
    (define anf1
      (lambda (lc-exp ctx)
        (match lc-exp
          [(? var?) (ctx lc-exp)]
          [`(lambda (,arg) ,lc-exp) (ctx `(lambda (,arg) ,(anf1 lc-exp id)))]
          [`(,rator ,rand) 
           (anf1 rator
                 (lambda (simple-rator)
                   (anf1 rand 
                         (lambda (simple-rand)
                           (if (val? simple-rator)
                               (if (val? simple-rand)
                                   (ctx `(,simple-rator ,simple-rand))
                                   (let ([rand-var (gensym "rand")])
                                     `(let ([,rand-var ,simple-rand])
                                        ,(ctx `(,simple-rator ,rand-var)))))
                               (let ([rator-var (gensym "rator")])
                                 `(let ([,rator-var ,simple-rator])
                                    ,(anf1 `(,rator-var ,simple-rand) ctx))))))))])))
    (anf1 lc-exp id)))


示例:
> (anf '(f (lambda (a) (f (b a)))))
'(f (lambda (a) (let ((rand526 (b a))) (f rand526))))
相信大家已经能很容易看懂了
 
 

 

 
 

玩弄Lisp系列第1.5弹?CPS逆变换  

2013-12-24 15:11:51|  分类: 默认分类 |  标签:lisp   |举报 |字号 订阅

 
 
    闲得无聊弄了这么个玩意,原理和前面的CPS变换一模一样,不重复说明了。

#lang racket
(require "cps.rkt")

(define lookup
  (lambda (s env)
    (cond [(assq s env) => cdr]
          [else s])))

(define ext
  (lambda (k v env)
    (cons (cons k v) env)))

(define env0 '())

(define expand-k
  (lambda (sexp k-exp env ctx)
    (match k-exp
      ['k (ctx sexp)]
      [`(lambda (,v) ,cps-exp)
       (uncps cps-exp (ext v sexp env) ctx)])))

(define uncps
  (lambda (cps-exp [env env0] [ctx id])
    (match cps-exp
      ;;simple-exp
      [(? self-evaluate? self) (ctx self)]
      [(? symbol? sym) (ctx (lookup sym env))]
      [`(quote ,x) (ctx cps-exp)]
      [`(lambda (,a k) ,cps-exp) (ctx `(lambda (,a) ,(uncps cps-exp env id)))]
      [`(,(? unary-op? rator) ,simp-rand)
       (uncps simp-rand env
              (lambda (rand)
                (ctx `(,rator ,rand))))]
      [`(,(? bin-op? rator) ,simp-v1 ,simp-v2)
       (uncps simp-v1 env
              (lambda (v1)
                (uncps simp-v2 env
                       (lambda (v2)
                         (ctx `(,rator ,v1 ,v2))))))]
      
      ;;cps-exp
      [`(if ,simp-test ,cps-then ,cps-else)
       (uncps simp-test env
              (lambda (test)
                `(if ,test
                     ,(uncps cps-then env ctx)
                     ,(uncps cps-else env ctx))))]
      [`(let ([k ,k-exp]) ,e)
       (expand-k (uncps e env id) k-exp env ctx)]
      [`(k ,simp) (uncps simp env ctx)]
      [`(,simp-rator ,simp-rand ,k-exp)
       (uncps simp-rator env
              (lambda (rator)
                (uncps simp-rand env
                       (lambda (rand)
                         (expand-k `(,rator ,rand) k-exp env ctx)))))])))

修改了一点的CPS变换代码:
#lang racket
(provide cps self-evaluate? id bin-op? unary-op?)

(define self-evaluate?
  (lambda (thing)
    (or (number? thing) (boolean? thing))))

(define id
  (lambda (v) v))

(define ctx0
  (lambda (v)
    `(k ,v)))

(define unary-op?
  (lambda (x)
    (memq x '(add1 sub1 zero? car cdr))))

(define bin-op?
  (lambda (x)
    (memq x '(cons + - * / < > = <= >=))))

(define cps
  (lambda (sexp [ctx id])
    (match sexp
      [(? self-evaluate?) (ctx sexp)]
      [(? symbol?) (ctx sexp)]
      [`(quote ,thing) (ctx sexp)]
      [`(if ,test ,then ,else) (cps test 
                                    (lambda (simple-test)
                                      (if (memq ctx `(,ctx0 ,id))
                                          `(if ,simple-test
                                               ,(cps then ctx)
                                               ,(cps else ctx))
                                          `(let ([k (lambda (v) ,(ctx 'v))])
                                             (if ,simple-test
                                                 ,(cps then ctx0)
                                                 ,(cps else ctx0))))))]
      [`(lambda (,a) ,e) 
       (ctx `(lambda (,a k) ,(cps e ctx0)))]
      [`(,rator ,rand) (cps rator
                            (lambda (simple-rator)
                              (cps rand
                                   (lambda (simple-rand)
                                     (cond [(unary-op? simple-rator)
                                            (ctx `(,simple-rator ,simple-rand))]
                                           [(eq? ctx ctx0)
                                            (list simple-rator simple-rand 'k)]
                                           [else 
                                            (let ([v (gensym "v")])
                                              (list simple-rator simple-rand
                                                    `(lambda (,v)
                                                       ,(ctx v))))])))))]
      [`(,(? bin-op? op) ,a ,b) (cps a
                                     (lambda (a)
                                       (cps b
                                            (lambda (b)
                                              (ctx `(,op ,a ,b))))))])))

然后是抄来的测试案例:

(define test
  (lambda (sexp)
    (equal? (uncps (cps sexp)) sexp)))

(define test-all
  (lambda ()
    (andmap test
            '(
              x
              (lambda (x) x)
              (lambda (x) (x 1))
              (if (f x) a b)
              (if x (f a) b)
              (lambda (x) (if (f x) a b))
              (lambda (x) (if (if x (f a) b) c d))
              (lambda (x) (if (if x (zero? a) b) c d))
              (lambda (x) (if t (if x (f a) b) c))
              (lambda (x) (if (if t (if x (f a) b) c) e w))
              (lambda (x) (h (if x (f a) b)))
              (lambda (x) ((if x (f g) h) c))
              (((f a) (g b)) ((f c) (g d)))
              (lambda (n)
                ((lambda (fact)
                   ((fact fact) n))
                 (lambda (fact)
                   (lambda (n)
                     (if (zero? n)
                         1
                         (* n ((fact fact) (sub1 n))))))))))))

测试通过:
> (test-all)
#t
 
*****************************************************
就是將一段代碼轉換爲等價的 Continuation Passing Style。這東西不好解釋,推薦去看這本書: Essentials of Programming Languages (搞 scheme 的都應該認識本書的作者, Daniel P Friedman,這人也是王珢在 Indiana 大學的導師)
 
他自己以前说过:自动的CPS变换. 用伪C代码解释一个:
比如, 变换前的:
int sum(int* arr, int len){
if(len <= 0) return 0; else return arr[0] + sum(arr+1, len-1);
}
调用: print(sum([1,2,3,4], 4)) //输出 10;

变换后的:
void sum_cps(int sum, int* arr, int len, void (callback*)(int)){
if(len <= 0) callback(sum);
else sum_cps(sum + arr[0], arr+1, len-1, callback);
}
调用:(sum_cps, 0, [1,2,3,4], 4, print);
这样变换以后, 自动变成尾递归. sum 每次递归调用需要把arr[0] 压栈, len大时会堆栈溢出, 而sum_cps,需要的栈为0, 不会堆栈溢出.
递归调用是FP的根本, 所以自动实现这种变换意义很大.
 
 
CPS的基本思想是将普通函数的return转换为调用另一个函数(即这个函数的continuation),由于函数永远都不会返回,我们也就不需要调用栈。举例来说呢,Chicken Scheme这样的编译器就会利用CPS来消除调用栈。
另外,如果一个程序写成了CPS形式的话,call/cc这个special form可以用一个普通函数来实现:

(lambda (f k) (f (lambda (v k0) (k v)) k))

由于call/cc一直是解释器性能优化的一个难点,不难理解CPS转换对于现代函数式语言的编译器、解释器的重要意义了。
 
************************************************************
http://www.cppblog.com/vczh/
http://zhuanlan.zhihu.com/dummydigit
****************************************

 

=============== End

猜你喜欢

转载自www.cnblogs.com/lsgxeva/p/10148511.html