初识Continuation
本文来自网易云社区
作者:陆艺
上学时看了SICP之后就对scheme这个看上去比较古怪的语言产生了兴趣. 虽然书里并没有涉及scheme太多语法以及语言上特性的一些东西, 作为一个喜欢折腾的人, 手贱翻了翻Google, 了解到有continuation这样一个吊吊的东西, 于是借着有限的脑力学习了下.
基本概念
continuation的中文一般都叫做"续延"(还蛮好听的), 不过解释起来比较麻烦, 还是结合代码来看更好理解一点.
首先scheme里的continuation(太长了, 下文缩写为cont吧)的基本形式是长这个样子的:
(call/cc
(lambda (cc)
; do some computation)
)
call/cc
是call-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)))
)
一步步来:
整个block可以看做
(+ 1 [])
再看
[]
内部:(lambda (cc) (+ 2 (cc 3)))
, 这个lambda的body是会被执行的(cc 3)
以3为参数调用cont, 则跳转到其定义的地方[]
将其替换最后变为
(+ 1 3)
, 结果为4(+ 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)
)
)
老样子一步一步看:
全局定义一个
return
(绑定啥值无所谓)替换cont形式:
(+ 1 [])
在lambda body中,
return
与cont[]
绑定!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)
为了大家(我自己)能简单的看懂, 我们走的慢一些:
分别定义了两个过程: 主要计算和次要计算; 以调用主要计算开始, 参数为次要计算本身
开始主要计算, 进入loop(第一次), 打印"Hefty computation: 5"
(1)处
call/cc
以次要计算为参数, 结合后者定义, 则有(为了方便, 分别记两个两个过程的参数为do1和do2):主要计算产生第一个cont,
(set! do1 []-1)
开始次要计算, 参数do2为上一步的
[]-1
, 进入loop(第一次), for-each打印列表中第一个"Straight up."到达(2)
(call/cc do2)
, 此时次要计算产生一个cont, 记为[]-2
, 同时完成do2也就是[]-1
的计算, 流程回到了(1), 且将do1绑定到了[]-2
从(1)后继续主要计算, 打印"Hefty computation (b)"
主要计算(3)处产生第二个cont
[]-3
, 形式和上次一样; 由于参数do1实际上是[]-2
, 则:跳转到
[]-2
即(2)处继续执行, 此时set!
将[]-2
的返回值即[]-3
绑定到do2for-each的第二个循环打印"Quarter after."
流程又到(2)处,
(call/cc []-3)
, 并产生当前的cont[]-4
, 作为[]-3
的参数; 执行[]-3
的计算, 于是又回到了(3)处do1绑定为
[]-4
, 且打印"Hefty computation (c)"接下来的过程类似上面, 即不断的在主要计算和次要计算之间跳来跳去(和协程的切换是一样的)
看上去似乎有点晕, 总结一下这个用法的关键在于:
(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:
http://community.schemewiki.org/?call-with-current-continuation
http://stackoverflow.com/questions/2694679/how-does-the-yin-yang-puzzle-work/36513942#36513942
本文来自网易实践者社区,经作者陆艺权发布
相关文章:
【推荐】 深入解读Service Mesh背后的技术细节
【推荐】 网易云数据库架构设计实践
【推荐】 微服务监控探索
初识Continuation的更多相关文章
- kubernetes1.9管中窥豹-CRD概念、使用场景及实例
欢迎访问网易云社区,了解更多网易技术产品运营经验. 前言 默认读者有kubernetes基础概念的背景知识,因此基础概念例如有状态.pod.Replica Sets.Deployments.state ...
- Question | 移动端虚拟机注册等作弊行为的破解之道
本文来自网易云社区 "Question"为网易云易盾的问答栏目,将会解答和呈现安全领域大家常见的问题和困惑.如果你有什么疑惑,也欢迎通过邮件(zhangyong02@corp.ne ...
- appium封装显示等待Wait类和ExpectedCondition接口
此文已由作者夏鹏授权网易云社区发布. 欢迎访问网易云社区,了解更多网易技术产品运营经验. 使用WebDriver做Web自动化的时候,org.openqa.selenium.support.ui中提供 ...
- Android动画效果之初识Property Animation(属性动画)
前言: 前面两篇介绍了Android的Tween Animation(补间动画) Android动画效果之Tween Animation(补间动画).Frame Animation(逐帧动画)Andr ...
- 初识Hadoop
第一部分: 初识Hadoop 一. 谁说大象不能跳舞 业务数据越来越多,用关系型数据库来存储和处理数据越来越感觉吃力,一个查询或者一个导出,要执行很长 ...
- python学习笔记(基础四:模块初识、pyc和PyCodeObject是什么)
一.模块初识(一) 模块,也叫库.库有标准库第三方库. 注意事项:文件名不能和导入的模块名相同 1. sys模块 import sys print(sys.path) #打印环境变量 print(sy ...
- 初识IOS,Label控件的应用。
初识IOS,Label控件的应用. // // ViewController.m // Gua.test // // Created by 郭美男 on 16/5/31. // Copyright © ...
- UI篇(初识君面)
我们的APP要想吸引用户,就要把UI(脸蛋)搞漂亮一点.毕竟好的外貌是增进人际关系的第一步,我们程序员看到一个APP时,第一眼就是看这个软件的功能,不去关心界面是否漂亮,看到好的程序会说"我 ...
- Python导出Excel为Lua/Json/Xml实例教程(一):初识Python
Python导出Excel为Lua/Json/Xml实例教程(一):初识Python 相关链接: Python导出Excel为Lua/Json/Xml实例教程(一):初识Python Python导出 ...
随机推荐
- ArcGIS Server Java 9.3 REST API的中文查询问题的解决方案
[2009.2.18补注]这个问题在SP1 for Linux中修复,SP1 for Windows下问题更加严重,如果打了SP1 for Windows,还想使用REST服务,就必须使用Linux或 ...
- POJ-3104 Drying---二分答案判断是否可行
题目链接: https://cn.vjudge.net/problem/POJ-3104 题目大意: 有一些衣服,每件衣服有一定水量,有一个烘干机,每次可以烘一件衣服,每分钟可以烘掉k滴水.每件衣服每 ...
- Android(java)学习笔记14:Java线程池
1. 线程池: 1)程序启动一个新线程成本是比较高的,因为它涉及到要与操作系统进行交互.而使用线程池可以很好的提高性能,尤其是当程序中要创建大量生存期很短的线程时,更应该考虑使用线程池. 2)线程池里 ...
- python:pymysql模块使用
一,基本使用 # 导入pymysql模块 import pymysql # 连接database conn = pymysql.connect(host=“你的数据库地址”, user=“用户名”,p ...
- 【[NOI2003]文本编辑器】
题目 发现这样一句话就会导致\(T\) ch[m][0]=++m; 并不是很知道为什么,可能这是某种未定义行为在不同编译器下会有不同后果? 至于这道题就很简单了,几个有关光标位置的操作就用一个变量模拟 ...
- react中性能优化的点
react提升代码性能的点 1.绑定如果改变作用域点话放在constructor里面做,这样可以保证整个程序的作用域绑定操作只会执行一次,而且避免子组件的无谓渲染. 2.内置的setState是个异步 ...
- FMDB初步使用小结
频繁的网络请求会给用户不好的体验,在最近开发的一个项目中有一个获取个人详细信息的界面,由于是子页面,进入页面后需要重新加载数据并刷新页面,而,每一次请求服务器再返回数据不仅用户体验不好,也花费手机流量 ...
- asp.net中Page.ClientScript.RegisterStartupScript用法小结
ClientScript.RegisterStartupScript(Page.GetType(), "Show", "alert('" + l_strConf ...
- centos安装nodejs二进制包
1.下载nodejs的二进制包 wget https://nodejs.org/dist/v6.3.1/node-v6.3.1-linux-x64.tar.xz 2.解压下载的安装包 tar xf n ...
- PL/SQL规范、块、过程、函数、包、触发器
1.pl/sql规范 标识符号的命名规范 1) 定义变量,用 v- 作为前缀 v-sal 2)定义常亮, 用 c- 作为前缀 c-rate 3) 定义游标,用 cursor作为后缀 emp_curso ...