從程式設計之初,我們就有錯誤處理的需求,常見的模式有幾種

C/Go 語言的模式(錯誤就是值)

func c() (int, error) {
  // ...
}

這種程式在使用時需要寫下大量的

if err != nil {
  // handle it; or
  return err
}

Java/Old C++ 的模式

就是一個 throw 語句把 exception 丟出去,具有 try ... catch 構造來處理錯誤

Haskell/Rust 的模式

monadic 的語句,比如(取自官網)

calculateLength :: ExceptT String IO Int
calculateLength = do
  liftIO $ putStrLn "Please enter a non-empty string: "
  s <- liftIO getLine
  if null s
    then throwError "The string was empty!"
    else return $ length s

這段程式的意思是擲出 String 作為錯誤,變換為 IO monad,回傳 Int 作為值。 Rust 則是用 Result 作為標記,寫 xxx? 就會被判斷為:如果 xxxErr(e)return,否則取出 Ok(v)v 並繼續進行計算。

全都是 monad

可以去 Introducing String Diagrams 找 Klesli 的 string diagrams 來看。所謂的 𝜂:𝐼𝑀 就是用來產出「沒有錯誤」,所以任何 pure computation 都從 𝐴𝐵 升格為 𝐴𝑀𝐵,由 𝜂 給出 𝑀 的部分。 𝜇:𝑀×𝑀𝑀 用來判斷是否有錯誤,只要至少一邊 𝑀 有錯誤,那輸出的 𝑀 就有錯誤,是否急迫求值就決定會只有第一個錯誤出現,或是兩個都回報。

Go Lift

這就是為什麽 Go lift 把 error 丟去一個中間結構然後代理運算可以運作,因為在代理函數裡面檢查 err != nil 就是急迫處理錯誤的部分,從而得到如

w := NewErrorWrapper("tcp", "server:6666")
 
w.Then(func(network, host string) (net.Conn, error) {
    conn, err := net.Dial(network, host)
    return conn, err
}).Then(func(conn net.Conn) error {
    _, err := conn.Write([]byte{`command`})
    return err
}).Final(func(e error) {
    panic(e)
})

這樣的程式

跳轉

exception 的模型可以擴充為 continuation 跳轉模型,這樣做的好處就是泛化了這個系統,exception 的 throw 語意本來就是轉交控制權給上層並且不會 resume 回來。

Compiler and Runtime Support for Continuation Marks 就有範例說明怎麼用 continuation 造出 exception 系統

(define handler-key (gensym))
 
(define (throw exn)
  (define escape+handle
    (continuation-mark-set-first #f handler-key #f))
  (if escape+handle
    (escape+handle exn)
    (abort "unhandled exception")))
 
(define-syntax-rule (catch handler-proc body)
  ((call/cc
    (λ (k)
      (λ ()
        (with-continuation-mark
          handler-key (λ (exn) (k (λ () (handler-proc exn))))
          body))))))

effect typing

跳轉模型的類型系統是 effect type system,就是在說明各種 abort (在 exception 中叫做 throw) 跳轉與 prompt (在 exception 中叫做 catch) 之間的 resume 協定。 koka 文件中有更多的說明。