本文来自网易云社区

作者:陆艺

上学时看了SICP之后就对scheme这个看上去比较古怪的语言产生了兴趣. 虽然书里并没有涉及scheme太多语法以及语言上特性的一些东西, 作为一个喜欢折腾的人, 手贱翻了翻Google, 了解到有continuation这样一个吊吊的东西, 于是借着有限的脑力学习了下.

基本概念

continuation的中文一般都叫做"续延"(还蛮好听的), 不过解释起来比较麻烦, 还是结合代码来看更好理解一点.

首先scheme里的continuation(太长了, 下文缩写为cont吧)的基本形式是长这个样子的:

(call/cc
  (lambda (cc)
    ; do some computation)
)

call/cccall-with-current-continuation的缩写, lambda的参数cc就是当前环境的一个cont, 可以理解为当前的调用栈, 它知道自己未来需要执行的过程. 那么怎么表示"未来的计算"这样一个概念呢? 在编程语言中自然就是函数了, 而在scheme中就是过程(procedure), 更一般的就用lambda来表示. cc就是这样一个只接受一个参数的lambda, 而调用(cc val)则是整个cont动作的关键----它将流程回cont定义的位置, 并以val代换为其计算出的结果.

来暴力理解一下, 把(call/cc ..) (rest code..)看做这样一个结构[] (rest code ..), 在(cc val)的时候直接回到了[]的地方, 并将其替换为了val.

看一个简单(似乎不是特别简单)的例子:

(+ 1 (call/cc)
      (lambda (cc) (+ 2 (cc 3)))
)

一步步来:

  1. 整个block可以看做(+ 1 [])

  2. 再看[]内部: (lambda (cc) (+ 2 (cc 3))), 这个lambda的body是会被执行的

  3. (cc 3)以3为参数调用cont, 则跳转到其定义的地方[]将其替换

  4. 最后变为(+ 1 3), 结果为4

  5. (+ 2 ..)的部分并没有luan3用 :P

基本用法

Jump out

这个用法有个名字叫"非本地退出(non-local exit)", 说白了就是现代编程语言里break啦, exit啦这些东西.

  • scheme没有这些关键字

  • scheme觉得它们不够old-school

  • scheme决定这么搞

(define (search wanted? lst)
  (call/cc
    (lambda (return)
      (for-each (lambda (e)
                  (if (wanted? e) (return e)))
                lst)
      #f
    )
  )
)

功能很直白, 从一个list里找到符合条件的元素, 不存在就返回#f. return的用法是直接从for-each的循环中跳出了. 这里最后的#f可不可以写成(return #f)? 当然可以, 不过整个lambda都计算完了, 自然是返回最后一个计算值, 所以可以直接省略啦.

另一个例子

(define (list-product list)
  (call/cc
    (lambda (exit)
      (let iter ((rest lst))        (cond
          ((null? rest) 1)          ((zero? (car rest)) (exit 0))          (else (* (car rest) (iter (cdr rest))))
        )
      )
    )
  )
)

对一个list完成fold计算乘积的操作, 中途如果遇到0则立即退出, 避免了不必要的遍历.

Jump back

在scheme里, cont和lambda一样都是一等公民, 它自然也可以被当做参数抛来抛去, 或者跟其他变量进行绑定.

(define return #f)(+ 1 (call/cc
      (lambda (cont) (set! return cont) 1)
     )
)

老样子一步一步看:

  1. 全局定义一个return(绑定啥值无所谓)

  2. 替换cont形式: (+ 1 [])

  3. 在lambda body中, return与cont[]绑定!

  4. 1作为返回值, 则(+ 1 [])结果为2

然而并没有结束, 此时return本身已经作为一个"+1器"存在了, 它代表的cont即(+ 1 []), 每次调用(return v)便会得到v + 1.

Jump out and Jump back

看到这里, 可以感觉到cont是能作为程序流控制的手段的, 用来完成一些比较时髦的动作, 比如大家都喜欢的协程 :P

所以接下来看一个模拟协程计算的较复杂的例子:

(define (hefty-computation do-other-stuff) ; 主要复杂计算
 (let loop ((n 5))
   (display "Hefty computation: ")(display n)(newline)
   (set! do-other-stuff (call/cc do-other-stuff)) ; (1)
   (display "Hefty computation (b)\n")
   (set! do-other-stuff (call/cc do-other-stuff)) ; (3)
   (display "Hefty computation (c)\n")
   (set! do-other-stuff (call/cc do-other-stuff))
   (if (> n 0) (loop (- n 1)))
 )
) (define (superfluous-computation do-other-stuff) ; 次要计算
  (let loop ()
    (for-each (lambda (item)
                (display item)(newline)
                (set! do-other-stuff (call/cc do-other-stuff))) ; (2)
              '("Straight up." "Quarter after." "Half past."  "Quarter til.")
    )
    (loop) ; this trigger inf loop
  )
) (hefty-computation superfluous-computation)

为了大家(我自己)能简单的看懂, 我们走的慢一些:

  1. 分别定义了两个过程: 主要计算和次要计算; 以调用主要计算开始, 参数为次要计算本身

  2. 开始主要计算, 进入loop(第一次), 打印"Hefty computation: 5"

  3. (1)处call/cc以次要计算为参数, 结合后者定义, 则有(为了方便, 分别记两个两个过程的参数为do1和do2):

  4. 主要计算产生第一个cont, (set! do1 []-1)

    1. 开始次要计算, 参数do2为上一步的[]-1, 进入loop(第一次), for-each打印列表中第一个"Straight up."

    2. 到达(2) (call/cc do2), 此时次要计算产生一个cont, 记为[]-2, 同时完成do2也就是[]-1的计算, 流程回到了(1), 且将do1绑定到了[]-2

  5. 从(1)后继续主要计算, 打印"Hefty computation (b)"

  6. 主要计算(3)处产生第二个cont[]-3, 形式和上次一样; 由于参数do1实际上是[]-2, 则:

    1. 跳转到[]-2即(2)处继续执行, 此时set![]-2的返回值即[]-3绑定到do2

    2. for-each的第二个循环打印"Quarter after."

    3. 流程又到(2)处, (call/cc []-3), 并产生当前的cont[]-4, 作为[]-3的参数; 执行[]-3的计算, 于是又回到了(3)处

  7. do1绑定为[]-4, 且打印"Hefty computation (c)"

  8. 接下来的过程类似上面, 即不断的在主要计算和次要计算之间跳来跳去(和协程的切换是一样的)

看上去似乎有点晕, 总结一下这个用法的关键在于:

(call/cc cc)实际上是将当前的cont作为参数contcc的结果, 然后通过执行cc跳转到cont创建时的地方; 之后通过set!将之前的cont(即cc的结果)保存下来, 然后再次通过(call/cc cont)跳回去, 如此往复.

最后一个例子

(let* ((yin  ((lambda (cc) (write-char #\@) cc) ; proc
              (call/cc (lambda (c) c))))        ; arg
       (yang ((lambda (cc) (write-char #\*) cc) ; proc
              (call/cc (lambda (c) c)))))       ; arg
  (yin yang)
)

这是网上流传很久的yin-yang puzzle, 程序会不停的交替打印"@*@**@***@ ...". 这个例子大家可以尝试自己拿纸笔推一下(虽然过程特别的绕, 但是通过不断代换的笨办法还是可以一战的), 或者可以看一下这个解答就比较好理解了, stay calm :P

时至今日我仍然没有完整的学完scheme.. 这里也只是简单的介绍了下自己对continuation及用法的基本理解, 欢迎各位大大多加指正.

ref:

本文来自网易实践者社区,经作者陆艺权发布

相关文章:
【推荐】 深入解读Service Mesh背后的技术细节
【推荐】 网易云数据库架构设计实践
【推荐】 微服务监控探索

初识Continuation的更多相关文章

  1. kubernetes1.9管中窥豹-CRD概念、使用场景及实例

    欢迎访问网易云社区,了解更多网易技术产品运营经验. 前言 默认读者有kubernetes基础概念的背景知识,因此基础概念例如有状态.pod.Replica Sets.Deployments.state ...

  2. Question | 移动端虚拟机注册等作弊行为的破解之道

    本文来自网易云社区 "Question"为网易云易盾的问答栏目,将会解答和呈现安全领域大家常见的问题和困惑.如果你有什么疑惑,也欢迎通过邮件(zhangyong02@corp.ne ...

  3. appium封装显示等待Wait类和ExpectedCondition接口

    此文已由作者夏鹏授权网易云社区发布. 欢迎访问网易云社区,了解更多网易技术产品运营经验. 使用WebDriver做Web自动化的时候,org.openqa.selenium.support.ui中提供 ...

  4. Android动画效果之初识Property Animation(属性动画)

    前言: 前面两篇介绍了Android的Tween Animation(补间动画) Android动画效果之Tween Animation(补间动画).Frame Animation(逐帧动画)Andr ...

  5. 初识Hadoop

    第一部分:              初识Hadoop 一.             谁说大象不能跳舞 业务数据越来越多,用关系型数据库来存储和处理数据越来越感觉吃力,一个查询或者一个导出,要执行很长 ...

  6. python学习笔记(基础四:模块初识、pyc和PyCodeObject是什么)

    一.模块初识(一) 模块,也叫库.库有标准库第三方库. 注意事项:文件名不能和导入的模块名相同 1. sys模块 import sys print(sys.path) #打印环境变量 print(sy ...

  7. 初识IOS,Label控件的应用。

    初识IOS,Label控件的应用. // // ViewController.m // Gua.test // // Created by 郭美男 on 16/5/31. // Copyright © ...

  8. UI篇(初识君面)

    我们的APP要想吸引用户,就要把UI(脸蛋)搞漂亮一点.毕竟好的外貌是增进人际关系的第一步,我们程序员看到一个APP时,第一眼就是看这个软件的功能,不去关心界面是否漂亮,看到好的程序会说"我 ...

  9. Python导出Excel为Lua/Json/Xml实例教程(一):初识Python

    Python导出Excel为Lua/Json/Xml实例教程(一):初识Python 相关链接: Python导出Excel为Lua/Json/Xml实例教程(一):初识Python Python导出 ...

随机推荐

  1. LA 4987 背包

    题意: 有n个施工队,给定他们的位置,有m个防空洞,给定位置,求将施工队放到m个防空洞里面,最少的总距离? n<=4000 分析: dp[i][j] 前 i 个施工队,放到前 j 个防空洞里面的 ...

  2. mac下登录服务器

    1.先通过帐号密码登录到服务器: ssh 用户名@服务器地址 https://jingyan.baidu.com/article/546ae1853132bf1148f28c42.html 2.登录后 ...

  3. 【luogu P3979 遥远的国度】 题解

    题目链接:https://www.luogu.org/problemnew/show/P3979 除了换根操作都是裸的树剖 所以换根时考虑: 1.我查询的根等于换的根:无影响 2.我查询的根是换的根的 ...

  4. 【luogu P2661 信息传递】 题解

    题目链接:https://www.luogu.org/problemnew/show/P2661#sub 一种利用并查集求最小环的做法: 对于每个同学看作一个点,每次信息传递是一条有向边,当出现最小环 ...

  5. JDBC执行存储过程的四种情况 (转)

    本文主要是总结 如何实现 JDBC调用Oracle的存储过程,从以下情况分别介绍: [1].只有输入IN参数,没有输出OUT参数 [2].既有输入IN参数,也有输出OUT参数,输出是简单值(非列表) ...

  6. ubuntu开启ssh连接

    1.安装openssh-server sudo apt-get install -y openssh-server 2.修改/etc/ssh/sshd-config配置 PermitRootLogin ...

  7. 我和我的广告前端代码(六):webpack工程合并、也许我不需要gulp

    随着年初开始使用webpack重构公司的广告代码,已经有将近一年的时间了,需求也渐渐的稳定了.我想也是时候将这几个工程整理一下,顺带着处理一些历史问题. 由于当年各个业务线没有整合.需求也没有固定,考 ...

  8. Xtrabackup备份与恢复MySQL

    1.innobackupex备份原理 .innobackupex启动并fork一个进程启动xtrabackup,然后等待xtrabackup备份InnoDB文件; .xtrabackup备份时存在两个 ...

  9. Showing All Messages : error: open /Users/apple/Library/Developer/Xcode/DerivedData/xxx-dkhmpttmnuppvbcxijlcxacfpzcl/Build/Products/Debug-iphoneos/xxx.app/EaseUIResource.bundle/arrow@2x.png: N

    2报错 Showing All Messages : error: open /Users/apple/Library/Developer/Xcode/DerivedData/xxx-dkhmpttm ...

  10. DLL DEF文件编写方法 VC++ 调用、调试DLL的方法 显式(静态)调用、隐式(动态)调用

    DLL 文件编写方法: 1.建立DLL工程 2.声明.定义要导出的函数 BOOL WINAPI InitDlg( HWND hTabctrl,TShareMem* pTshare,CRect* prc ...