Status: Draft
回復可能な失敗は,関数型の throws E 節と throw 式で表現する.
値の不在は Option<T> で表現する.
回復不能な異常は panic で処理する.
? operator は throwing な呼び出しの error を呼び出し元へ伝播する短絡評価 operator として導入する.
try / catch 式は送出された error を捕捉してハンドルする.
throws E: 回復可能な失敗 (error channel)
Option<T>: 値が存在しない
panic: 回復不能な異常
回復可能な失敗を Result<T, E> で表現することはしない.Result は組み込み型ではない.
関数が送出しうる error の型は,戻り値型に続く throws E 節で宣言する (0008).
fn read(path: Path) -> String throws IoError uses { fs }
fn parse(input: String) -> Ast throws ParseError uses {}
throws Eを持たない関数は error を送出しない.これはthrows Neverと等価である.Never型の値は存在しないため,throws Neverを宣言する関数は error を送出しえない.Eは単一の型である.複数種類の error を扱う場合は enum を用いる.throwsはusesとは独立した channel であり,capability をthrowsに書くことはできない.
enum LoadError {
NotFound
Decode(DecodeError)
}
fn load(path: Path) -> Config throws LoadError uses { fs }
throw e は error e を送出する.
throw IoError::NotFound
throw eは通常の値を返さない.型はNeverであり,任意の期待型に適合する.throw eが現れる関数はthrows Eを宣言していなければならず,eの型はEに適合していなければならない.ただしthrow eがtryblock 内にある場合は,その error はcatchに捕捉される (後述).throwは capability ではないため,usesには現れない.
fn lookup(id: UserId, table: Table) -> User throws NotFound uses {} {
match Table.get(table, id) {
Some(user) -> user
None -> throw NotFound
}
}
throws E を持つ関数の呼び出しを throwing な呼び出し と呼ぶ.throwing な呼び出しの成功値の型は T であるが,error を素通りさせることはできない.
throwing な呼び出しは,次のいずれかの位置に置かなければならない.これを満たさない throwing な呼び出しはコンパイルエラーとする.
?を付けて error を呼び出し元へ伝播する.tryblock 内に置き,catchで捕捉する.
? は throwing な呼び出しの error を,現在の関数の throws 節へ伝播する短絡評価 operator である.
? は throw, 早期 return に対する糖衣構文と捉えてよい.
expr が T throws E を持つ throwing な呼び出しの場合,以下のコードは等価である.
let value = expr?
let value = try {
expr
} catch {
e -> throw e
}
expr? を使う関数の戻り値の型は throws E を宣言していなければならない.
fn load(path: Path) -> Config throws IoError uses { fs } {
let text = read(path)?
parse(text)?
}
read(path): String throws IoError uses { fs }
parse(text): Config throws IoError uses {}
read と parse の error 型は IoError であり,load の throws IoError と一致するため,? はそのまま伝播できる.
? が伝播する error 型は,現在の関数が宣言する error 型と一致していなければならない.
fn load(path: Path) -> Config throws LoadError uses { fs } {
let text = read(path)? // read は throws IoError なので reject
parse(text)?
}
error 型が異なる場合は明示的に変換する.
fn load(path: Path) -> Config throws LoadError uses { fs } {
let text = try {
read(path)
} catch {
e -> throw LoadError::Io(e)
}
parse(text)?
}
後続仕様で From / Into 的な error 変換を導入してもよい.
? は throwing な呼び出し専用であり,Option<T> には使えない.Option<T> は通常の値であり,match またはコンビネータ(Option.map / Option.and_then / Option.unwrap_or など,spec 0031 で定義する)で扱う.
fn firstName(user: User) -> Option<String> uses {} {
match user.profile {
Some(p) -> p.firstName
None -> None
}
}
throws E の error channel と Option<T> の間に暗黙変換は無い.不在 (None) を回復可能な失敗として呼び出し元へ伝播したい場合は,Option.ok_or(o, e) -> T throws E(spec 0031)で明示的に error channel へ橋渡ししてから ? を使う.
fn require_profile(user: User) -> Profile throws NotFound uses {} {
Option.ok_or(user.profile, NotFound)?
}
? は postfix operator であり,対象の式を1回だけ評価する.
let x = f()?
f()を評価する- 成功値なら値を取り出す
- error が送出されたら現在の関数の
throwsへ短絡する
fn combine() -> Int throws E uses {} {
let x = a()?
let y = b()?
x + y
}
a() が error を送出した場合,b() は評価されない.
? 自体は capability effect を発生させない.expr? の effect は expr の effect と同じである.
let text = read(path)?
read(path): String throws IoError uses { fs } の場合,この式全体も uses { fs } を要求する.
try / catch は送出された error を捕捉してハンドルする式である.
let cfg = try {
load(path)
} catch {
IoError::NotFound -> defaultConfig()
e -> panic("unrecoverable")
}
try { block } catch { arms }は式である.blockを評価する.blockが成功値vで完了した場合,try/catch式全体の値はvであり,catchは評価されない.blockの評価中に erroreが送出された場合,eをcatchの arm に対して上から順に pattern match する.一致した arm の本体を評価し,その値が式全体の値となる.tryblock 内では throwing な呼び出しに?を付ける必要はない.送出された error はすべてcatchに routing される.
try / catch 式の型は,block の成功値の型と,すべての catch arm 本体の型の共通型である.match と同様に,すべての分岐は同じ型を返さなければならない.
catch の arm は,block 内で送出されうる error 型を網羅していなければならない (match の exhaustiveness と同じ).wildcard arm を用いてよい.
try {
read(path)
} catch {
e -> defaultText()
}
block 内で送出される error を catch がすべてハンドルした場合,try / catch 式自体は error を送出しない.したがって throws を宣言しない関数でも,throwing な呼び出しを try / catch で包めば呼び出せる.
fn loadOrDefault(path: Path) -> Config uses { fs } {
try {
read(path)
parse(path)
} catch {
e -> defaultConfig()
}
}
catch arm の本体内で再度 throw すれば error を再送出できる.その場合,関数は対応する throws を宣言していなければならない.
try / catch の effect は block と到達しうる catch arm 本体の effect の和集合である.
effects(try/catch) =
effects(block)
∪ effects(reachable catch arm bodies)
panic は組み込みの回復不能エラーである.
panic は現在の通常評価を終了し,プログラムを回復不能な異常終了へ移行させる.
panic は通常の値を返さず,型は Never であり,任意の期待型に適合する.
fn head<T>(xs: Array<T>) -> T uses {} {
match Array.get(xs, 0) {
Some(x) -> x
None -> panic("empty array")
}
}
panic は回復可能な失敗には使ってはならない.回復可能な失敗は throws または Option で表現する.
panic は throws でも uses でもなく,どちらの channel にも現れない.
WASM / WAMR では panic は runtime trap, runtime-provided panic handler に lowering しても良い.
WAMR embedder は panic message をログに出して異常終了しても良いが,通常評価へ復帰してはならない.
プログラムのエントリポイント main の throws は Never でなければならない (MUST).
throws Never は non-throwing(error を送出しない)を意味し,throws 節の省略と等価である.したがって main は throws 節を省略するか,throws Never を宣言する.Never 以外の error 型を main の throws に宣言した場合はコンパイルエラーとする.
main の本体が throwing な呼び出しや throw を含む場合は,main 自身が try / catch で error を処理するか,panic で異常終了させなければならない.error を main の外へ伝播することはできない.
throws E を持つ関数は,IR レベルでは内部的に成功値と error の tagged union (Ok / Err) を返す関数へ lowering してよい.これは実装上の表現であり,表面言語には現れない.
throw e
%err = make_internal_err e
return %err
let text = read(path)?
%r = call @read(path)
switch_internal %r {
Ok => block_ok
Err => block_err
}
block_ok:
%text = internal_payload %r 0
continue %text
block_err:
%e = internal_payload %r 0
%out = make_internal_err %e
return %out
try {
read(path)
} catch {
e -> defaultText()
}
%r = call @read(path)
switch_internal %r {
Ok => block_ok
Err => block_catch
}
block_ok:
%v = internal_payload %r 0
continue %v
block_catch:
%e = internal_payload %r 0
%d = call @defaultText()
continue %d
panic("unreachable")
%msg = const.string "unreachable"
call @__emela_panic(%msg)
unreachable
throws A | Bのような union error 型を許すか.- error 型の暗黙変換 (
From/Into) を導入するか. tryblock 内からcatchを飛び越えて関数のthrowsへ伝播する手段 (?の許可) を入れるか.