ChiefSailor的歪酷Blog
 
« 上一篇: Linux下的汇编程序设计 下一篇: 李开复博士给中国计算机系学生的建议 »
chiefsailor @ 2006-05-27 11:07

偶素研究程序设计语言的,近来一直研究continuation(连续),这是一个从lisp语言发展出来的一个概念,近几年不断应用发展到ML,python,Ruby等语言中,促进了程序设计概念方面的一些革新,也改变着我们对程序的看法。

可能内容不太好理解,大家都没兴趣。但我希望能够起一个抛砖引玉的功能,能够有一个对程序设计语言有兴趣的人能够觉得我讲的东东有意思,俺就觉得不再孤独,非常满足了:P

1.什么是continuation?
  首先我们要理解求值上下文这个概念,把环境限定在lisp语言。比如(+ (* 5 2) 10),这是一个lisp表达式,我们看看这个表达式的计算过程,我们区分现在正在求的值(当前计算)和剩下待求的值(剩余计算): 
  1.[+, (_ (* 5 2) 10)]
  2.[(* 5 2),((+ _) 10)]
  3.[*,((+ ((_ 5) 2)) 10)]
  4.[5,((+ ((* _) 2)) 10)]
  5.[* 5, ((+ (_ 2)) 10)]
  6.[2,((+ ((* 5) _)) 10)]
  7.[* 5 2 , (+ _ 10)]
  8.[10, (+ _ 10)]
  9.[+ 10, (_ 10)]
  10.[10, (+ 10 _)]
  11.[+ 10 10, (_)]
  12.[20,_]
  
  大家可以很清楚看到当方括号左边的表达式计算到一个值(常数或者函数)[需要注意一下就是(+ 10)也是一个函数,所以也是一个值],就把这个值发 送给右边的表达式,然后右边的表达式进行解析,找下一个要计算的值。我们把右边这个表达式就叫做程序的continuation,表示剩下来将要进行的计 算。

  现在大家不难理解,表达式的continuation是隐含在表达式结构中的,进行表达式计算过程中,变换表达式的同时就在变换 contination。我们没办法利用这个continuation做什么,但同时想想,如果在某个点把continuation保存下来放到一个变量 中,然后什么时候再把这个变量调出来使用,这样,我们就有了一个我们自己定制的程序状态,任意构造这其中的1....10计算过程。计算将变得时分强大。

  再来看个例子
  (define cont (lambda () #f))
  (+ (call/cc (lambda (k) 
      ( set! cont k)
      (* 5 2)
   10)
  我们看看它的计算过程
   ......
  [(call/cc (lambda (k) (set! cont k) (* 5 2))), (+ _ 10)]
  [(set ! cont k) (* 5 2) ,(+ _ 10)]
  这时把cont的状态更新为了记录下来了为(+ _ 10)
  其他步骤都一样,呵呵,我们得到了最关键的一步,记录程序中任何时刻剩余计算(contintuation)的能力。
  
  当我们计算(cont 20)
  时,计算过程从
  [20, (+ _ 10)]
  开始,我们得到了 30

  嗯,这就是call/cc最初的想法。
------------------------------
continuation的应用(1):
从循环中退出

看一段程序:
(define product
  (lambda (ls)
    (let f ((gs ls))
      (cond
        ((null? gs) 1)
        ((= (car gs) 0) 0)
        (else  (* (f (cdr ls)) (car gs)))))))

这段程序输入一个list, 然后计算list里面所有元素的乘积
先解释一下一些常数:
null?表示是否为空
car 表示取表头元素
cdr 去list中出去表头后剩下的list
let f (参数 参数值)  (...f) 表示f是递归函数。
(define 函数名 (lambda (参数) (函数体)))表示定义函数
(product '(1,2,3,4,5)) 我们将会得到120

仔细分析一下这个函数,我们发现有个毛病

它是个递归的结构,如果参数所在那个表中有个是0的话,我们指导程序肯定是会得到0的,但product中还是会多次乘上这个0,有没有办法直接退出了??

来看看call/cc的解决方案吧:
(define product
  (lambda (ls)
    (call/cc (lambda (k)
               (let f ((gs ls))
                   (cond
                    ((null? gs) 1)
                    ((= (car gs) 0) (k 0))
                    (else  (* (cdr gs) (car gs)))))))
    ))

来看(product '(1,2,3,0,4))
清晰看到几个中间状态
  k 记录的剩余计算就是(_)
  当碰到
  (= (car  '(0,4) 0) 候,计算转变为
  [0,(_)]
  程序直接退出,product的返回值为0,狠像C语言中的exit()函数,但是这里我们从原理上分析了exit:-P
----------------------------------------
用continuation循环

试想如果lisp程序中没有for和while,程序将如何写,如果我们还希望有异常退出呢?

看看下段程序

(define get-continuation-with-values
  (lambda (values)
    (let ((receiver (lambda (proc) (cons proc values))))
     (call/cc receiver))))

(define loop-with-current-value
   (lambda (values)
     (let ((here-with-values (get-continuation-with-values values)))
           (let ((continue (car here-with-values)
                 (current-values (cdr here-with-values)))
                   (write-line current-values)
                   (if < (car current-values 100)
                         (continue (cons continue
                                    (map (lambda (x) (+ x 1))
                                         current-values)))
                   (write-line "done"))))))

我们看(loop-with-current-value '(1 2 3))

执行的效果是,打印list,然后list里每个元素+1,再打印,循环,直至list里的第一个元素到达100,退出。

1 2 3
2 3 4
.....
100 101 102
done

这个程序体现了几个特点
1.高阶函数在函数式程序设计语言中的应用
看map函数,输入一个函数f,然后再输入一个list,结果是f作用在list中的每个元素上,然后再组成一个新的list

2.lisp语言中untype特性
大家观察一下here-with-values,再本事例中,它有四个元素,第一个元素是一个continuation,也就是说它的type是一个函 数,后面三个元素是整数,这我们每次传送到下循环体中的是一个头元素为函数,后面元素为整数的混合连表,lisp是可以的,其它基于 strong type的语言则是不行的,比如ML,这也就是lisp的魅力所在,也是lisp的困难所在!!

3.隐式的保存continuation
大家看(let (here-with-values (get-continuation-with-values values)))
这句,执行的是
(call/cc (lambda (proc (cons proc values)))
这样当前的continuation被保存在proc中,然后保存到了here-with-values中

然后(let (continue (car here-with-values))就把proc取到continue中了

再执行(contiune (cons continue .....)
时,程序控制转到
( define loop-with-current-value
  (lambda (values)
     (let ((here_with_values (_))
       
        (let ((continue (car here-with-values)
                 (current-values (cdr here-with-values)))
                   (write-line current-values)
                                   (if < (car current-values 100)
                         (continue (cons continue
                                    (map (lambda (x) (+ x 1))
                                                 current-values)))
                   (write-line "done"))))))
  )

这就是这段程序的妙处所在!
------------------------------------------
continuation的应用(4)
模拟调试器

lisp语言的调试困难,没有好用的调试器:-(,没办法解决么?
continuation为我们提供了解决方案,假定我们现在在lisp的解释器环境中,希望能够在源程序中修改某个点,让程序执行到断点的时候断掉,然后可以从解释器环境中重新恢复到断点处,相当于基本实现一个断点功能。

首先第一不就是定义一个断点函数,这个函数输入一个参数时,保存断点,然后退出到解释器环境。
(define break
   (lambda (arg)
      (let ((exit) (lambda (cont)
                     (set! resume-computation (lambda () (cont arg) ;;保存断点
                     (write-line "execute paused. Try (recume-computation)");打印信息
                     ((escaper (lambda () arg)))))) ;退出到解释器环境
       (call/cc exit-procudure))

现在困难的地方在怎么退出到解释器环境中
直接的想法就是构造一个解释器环境中的continuation,以为lisp解释器也是一个lisp程序啊,这个continuation必须对全部解释执行的程序可见,所以为全局变元,另外要退回到正确的位置。

1.构造全句变元
(define escape-thunk (lambda () "escape-thunk initialize")

2.将全局continuation定到正确的位置
(define escape-thunk-init
   (lambda (continue)
      (set! escape-thunk continue)))

((call/cc escape-thunk-init))

呵呵,特别注意call/cc外面的两个括号,为什么是两个括号,大家想清楚了么??如果是一个括号的话,保存的是记录点到现在执行函数的一个镜像,如果是两个括号则是(_),退后到定义的括号里面了,这点真是狠神奇。

3.利用全局变量escape-thunk构造一个退回函数了
(define escaper
   (lambda (procedure)
      (lambda args 
           (escape-thunk (lambda () (apply produre args))))))))

来看源程序
(define flatten-list
    (lambda (arg)
        (cond
           ((null? arg) '())
           ((not (list? arg)) (list (break arg)))
           (else
               (append (flattern-list (car arg))
                       (flattern-list (cdr arg))))))))))

只是加了一个break
结果了
(flattern '(1 2 3))
"Execution paused. Try (recume-computation)"
value 3
==>(recume-computation)
"Execution paused. Try (recume-computation)"
value 2
==>(recume-computation)
"Execution paused. Try (recume-computation)"
value 1
==>(recume-computation)
"Execution paused. Try (recume-computation)"
value 3 
(1 2 3)

大家想想序列为什么是3,2,1??
--------------------------------
continuation的应用[5]
协子程序(coroutine)

编写过进程间通信的程序员多多少少都知道进程间通信、进程同步等概念。多进程(多线程)是这样一种模式,多个进程在一起运行,进程之间并不清楚对方,进程 知道调度器,调度器知道所有的进程,比如某个进程A正在运行,现在发生了一次中断,A被调度器调度出去,B被调度进来,A和B之间并无直接通信。

协子程序是这样一种模型,没有进程调度器了,进程彼此之间都知道对方,然后进程自己把控制传给另外一个进程,比如打牌程序A和B,A打完了,把控制传给B说,该你打了。

我们来看continuation编写协子程序实例
协子程序构造者(coroutine-maker)
(define coroutine-maker
   (lambda (proc)
       (let ((saved-continution '())
          (let ((update-continuation! (lambda (v)
                                         (write-line "updateing")
                                         (set! saved-continuation v))))
             (let ((resumer (resume-maker update-continuation!))
                   (first-time #t))
                   (lambda (value)
                     (if first-time
                       (begin 
                          (set! first-time #f)
                          (proc resumer value))
                       (saved-continuation value))))))))))

可以看到构造者输入一个函数,然后构造一个恢复者(resumer),判定是否第一次执行,如果第一次执行就改标志位,并调度另一个构造者

来看resume--maker
(define resume-maker
     (lambda (update-proc!)
        (lambda (next-coroutine value)
           (let ((receiver (lambda (continuation)
                             (update-proc! contiuantion)
                              (next-coroutine value)
            (call/cc receiver))))
resume-maker完成两个工作,先保存执行点的continuation,然后调度下一个协子程序执行

然后就是ping pang 程序了
(define ping
   (let ((ping-procedure (lambda (resume value)
                            (write-line "pinging 1")
                            (resume pong value)
                            (write-line "pinging 2")
                            (resume pong value)
                            (write-line "pinging 3")
                            (resume pong value))))
   (coroutine-maker ping-procedure)))

(define pong
   (let ((pong-procedure (lambda (resume value)
                            (write-line "pinging 1")
                            (resume ping value)
                            (write-line "pinging 2")
                            (resume ping value)
                            (write-line "pinging 3")
                            (resume ping value))))
   (coroutine-maker pong-procedure)))

(ping 1)
执行结果是
"pinging 1"
"ponging 1"
"pinging 2"
"ponging 2"
"pinging 3"
"ponging 3"
value:1

这个程序挺有意思的地方在于 :
ping-procedure和pong-procedure都维护了一个内部的save-continuation,然后
都只执行了一次(proc resumer value),然后当从别处返回控制后都是执行(saved-continuation value)

resume这个高介函数完成了两个功能,记录当前的continuation到save-contination, 然后执行ping value或者pong value

let 语句的语义是先于返回值之前执行的,所以resumer和update-continuation都只构造一次,不存在重复构造,需细细体会let语句在函数式程序设计语言中的应用

==========================
Author:
magicring
From:kyxk



最新评论


julie

2006-05-28 14:39

牛掰~
啥都没看懂...
想当年我也是一学数学滴!!!
怒!!!



chiefsailor

2006-05-29 09:12

谢谢Julie的友情支持,好久没有人回复了~~
介绍下LISP:LISP,表处理的一个首字母缩略词,它是为简单数据串处理而设计的。显现出由约翰?McCarthy它是1959 年John McCarthy为人工智能编程设计的通用语言。它仍然是最旧的编程语言的当中一个在相对地宽使用。它是一个最老的相对广泛使用编程语言。在LISP里,所有的计算都表示为至少一个对象函数。对象可能是其它函数、数据项(比如常数或变量),或数据结构。LISP的符号表达式计算能力以而不是数字使它便于AI 程序。



阿睿

2007-08-07 10:49 匿名 220.160.*.*

哦,还是看得很晕。怎么确定contiuation保存的是程序的哪一步骤呢?

尤其是看到 for while的替代实现就开是混了...



chiefsailor

2007-08-08 08:45 匿名 159.226.*.*

这边好久不更新了,早晨忽然收到歪酷给发的说有新留言的邮件,lisp的话我也不是很懂,不过大概瞟了一眼,应该是用括号来控制函数流程的,耐心分析下就应该有结果

我的新博客地址见本博置顶贴。


评论 / 个人网页 / 扔小纸条
* 昵称

已经注册过? 请登录

新用户请先注册 以便能显示头像及追踪评论回复

Email
网址
* 评论
表情
 


 

分类小组论坛
杂谈 , 娱乐、八卦 , 文学、艺术 , 体育 , 旅游、同城 , 象牙塔 , 情感 , 时尚、生活 , 星座 , 科技

请注意遵守中华人民共和国法律法规, 如威胁到本站生存, 将依法向有关部门报告, 同时本站的相关记录可能成为对您不利的证据.

相关法律法规
全国人大常委会关于维护互联网安全的决定
中华人民共和国计算机信息系统安全保护条例
中华人民共和国计算机信息网络国际联网管理暂行规定
计算机信息网络国际联网安全保护管理办法
计算机信息系统国际联网保密管理规定

日历
网志分类
· 所有网志
· 生活点滴
· 学习生涯
· 物语
· 我贫我掰
· C/C++
· 技术文档
· .NET
· JAVA
· Linux
· Network
· Database
精华文章
人生必看十大动画片
历届奥斯卡奖获奖名单回顾
帮你参悟人生十个故事
冷评热论央视记者
考研吧,悟空!
国内热门软件作者真人照片(多图)
C++程序设计之四书五经(上)
C++程序设计之四书五经(下)
C++资源之不完全导引(上)
C++资源之不完全导引(下)