Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

处理

(Handling)

让我们从定义自己的 异常效应 开始。effect 定义一个新效应类型,声明相关操作。下面我们先使用最通用的 控制(ctl) (之后会详解它)操作写个例子:

effect raise
  ctl raise( msg : string ) : a

这定义了一个效应类型 raise ,声明了一个类型为 (msg : string) -> raise a 的操作 raise 。完整定义决定了效应类型的签名,有了其签名之后,我们就可以使用这个操作了:

fun safe-divide( x : int, y : int ) : raise int
  if y==0 then raise("div-by-zero") else x / y

在这里我们可以看到,安全除法函数带有 raise 效应(因为我们在函数体中使用了 raise 操作)。这样的效应类型意味着,我们只能在 raise 被处理的上下文中对该函数进行求值(换句话说,即在 raise 被“动态绑定”的上下文中,或者在我们“拥有raise能力(capability)”的上下文中)。

我们可以通过为 raise 操作给出一个具体的定义来处理这种效应。例如,我们可能会始终返回一个默认值:

fun raise-const() : int
  with handler
    ctl raise(msg) 42
  8 + safe-divide(1,0)

调用 raise-const() 的结果是42(而非50)。当在 safe-divide 中调用 raise 时,控制权会让渡(yield)给动态调用栈中距离调用点最近的 raise 处理器——即“最内层“处理器:若同一效应有多个嵌套处理器,总是最靠近调用点的那个接管,类似别的语言异常处理中优先匹配最近的 catch 块;本例只有一个处理器,所以由它接管。控制权移交后,处理器展开调用栈,再按操作的定义求值,在此例中直接返回 42。现在我们明白了为何称其为控制操作:raise 改变了常规的线性控制流,从原始调用点直接跳到最内层处理器。处理器已消除了 raise 效应,raise-const 重新变为无效应的 total 函数。

handler{ <ops> } 表达式本身是一个函数,它期望接收一个函数参数,并在此 handler 所在作用域内应用它,例如对于 (handler{ <ops> })(action) ,会在 (action) 内应用它。这自然与 with 语句配合得很好。对于单个操作,我们可以省略 handler 关键字,其翻译如下:

with ctl op(<args>){ <body> }
可被翻译为
with handler
  ctl op(<args>){ <body> }

借助这一翻译,我们可以将之前的示例更简洁地写成:

fun raise-const1() : int
  with ctl raise(msg) 42
  8 + safe-divide(1,0)

它最终会扩展为 (handler{ ctl raise(msg){ 42 } })(fn(){ 8 + safe-divide(1,0) })。

在效应只声明了一个操作时,若效应和操作的名称相同,我们可以仅通过声明其操作来定义这样的效应,这隐式地定义了一个同名的效应类型:

effect ctl op(<parameters>) : <result-type>
可被翻译为
effect op {
  ctl op(<parameters>) : <result-type>
}

这意味着我们可以将 raise 效应的签名更为简洁地写成:

effect ctl raise( msg : string ) : a