开放作用域
开放作用域
代数效应解决了两类不同的问题,我认为应该明确把这两种情况分开:
在绝大多数情况下,我们只是需要暂停处理下副作用,随后便恢复程序的执行,也不需要添加后处理的内容。此时的resume
是单次执行,一去不回的,并不依赖于try...with
块的存在,应该使用捕获点位置的一次性全续体。
在少数情况下,比如需要回溯、后处理等情况,我们需要不止一次地恢复到捕获点,重复执行后面某一段的代码,此时resume
是有去有回,有可能多次执行的,明确的返回点必须依赖try...with
的标记。
注意到不管是异常还是代数效应寻找处理器的过程,都和动态作用域寻找函数差不多,尤其是第一种情况,为什么不呢?
函数拓展
我的思路是拓展函数的定义……处理器会获取一个只能被调用一次的当前栈的续体对象,它就像return
关键字一样可以在当前处理器被有去无回地调用一次,但是与return
不同的是,它可以被当前的词法作用域捕获(甚至被传出去),因此隔绝了副作用,又不会让副作用往外传递。
那么普通函数的参数列表,可以拓展为(a,b,…;foo)
,这个foo
就是CPS格式的续体,可以任意命名,但是必须且只能被调用一次。
这样做危险的一点是,只要续体被作为值传递出去了,程序就不会隐式return
,如果传递的这个续体丢失了,程序的执行就在这个函数的位置终止了。
但是这能够让程序员无成本地隔离副作用。
我有一个主意🤓👆
既然它出问题在于续体可能不被使用而被销毁,那我跟踪它的销毁不就行了。让续体的销毁同样引发程序的后续执行,线程不就不会意外终止了吗?
如果所有权能像 Rust 一样明确的话,那么更加没有问题了!
续体是可选的,不使用(;
)语法就还是原本的默认情况。
至于错误处理,代数效应是足够的,而且也明确了从调用栈往上找的模型。
由于这个模式能统一隔离副作用,因此它本身能实现异步。
通俗理解就是,你可以命名函数的return
,然后把这个return
当成函数到处传,但是需要保证它会且只会被调用一次。
一个伪代码例子
function loadPic(url, others; ret=()->{raise error}){//替换默认行为,这里是摧毁时抛出错误,但是也可以改成返回特定值,函数体保证了不会提前执行,默认行为只能在创建时替换
ret = val -> ret(process(val))//在返回前插入流程
setTimeout(()->{
ret(pic)
//如果上一行没有调用,在这里没有进一步显式处理 就会在这里被丢弃
},1000)
}
show(loadPic(url, others))