TL; DR
-
Optionalに?を付ける orResultの->forward_err()を呼ぶと、失敗した場合に早期returnしてくれる
はじめに
Onyxでは Optional や Result 型により失敗しうる処理を表現可能です。大域脱出が無い場合エラーハンドリングが煩雑になりがちですが、Onyxには便利なシンタックスシュガーが用意されていました。
OptionalとResult
本題に入る前に Optional, Result 型について見ていきます。馴染みのある方は次の章まで読み飛ばしてください。
Optional
空である可能性がある値を表します。Onyxにはポインタ型もあるのですが、 null と違いパターンマッチで静的にハンドリングできるので安全です1。
// 定義
println(Optional.empty(str)); // None
println(Optional.make("foo")); // Some("foo")
// 使用例
m := map.literal(str, i32, .[
.{"foo", 1},
.{"bar", 2},
]);
opt := m->get("foo");
// 値がある場合、ない場合でパターンマッチ
switch opt {
case .None {
println("not found");
}
case v: .Some {
println(v);
}
}
Result
Result は「値もしくはエラー」を表す型です。失敗しうる操作の戻り値に使用されます。こちらもエラーハンドリングはパターンマッチで行えます。
use core {println, Result}
div :: (a: i32, b: i32) -> Result(i32, str) {
if b == 0 {
return .{Err="divided by zero"};
}
return .{Ok=a / b};
}
main :: () {
println(div(6, 3)); // Ok(2)
println(div(6, 0)); // Err("divided by zero")
}
Optional, Resultのエラーハンドリング
?で早期リターン
毎回「もし中身が無いならば早期リターン」と書くのは面倒なので、Onyxには ? という演算子が用意されています。
戻り値が ?T 型の関数内でOptional型の式に対して ? を使用すると
- 値が存在する場合は中身が取り出される
- 存在しない場合は
Noneを早期リターンする
という風にはたらきます。
// 本文で?を使う場合、戻り値型をOptionalにする必要がある
find_and_capitalize :: (arr: []str, f: (str) -> bool) -> ? str {
// ?でOptionalの中身を取り出す。無い場合は早期リターン
s := iter.as_iter(arr)->find(f)?;
return string.to_uppercase(s);
}
main :: () {
values := str.["foo", "bar", "baz"];
println(find_and_capitalize(values, (s: str) => s[0] == 'b')); // Some("BAR")
println(find_and_capitalize(values, (s: str) => s[0] == 'x')); // None
}
??でガード
値が無い場合に早期リターンではなくデフォルト値を返したい場合は ?? を使用します。
opt := Optional.empty(str);
println(opt ?? "default_value"); // default_value
Result#forward_errで早期リターン
Result もエラーハンドリングを簡潔にすることが可能です2。
戻り値が Result型の関数内で Result型の式に対して forward_err() メソッドを呼び出すと
- 値が存在する場合は中身が取り出される
- 存在しない場合はエラーを早期リターンする
という風にはたらきます。
// 説明のために実装しているが、標準ライブラリにもfoldは用意されている
fold :: (arr: []$T, init: T, combine: (T, T) -> Result(T, str)) -> Result(T, str) {
result := init;
for value: arr {
// forward_err使用
// エラーだった場合早期リターン
result = combine(result, value)->forward_err();
}
return .{Ok=result};
}
add :: (a: i32, b: i32) -> Result(i32, str) {
return .{Ok=a + b};
}
div :: (a: i32, b: i32) -> Result(i32, str) {
if b == 0 {
return .{Err="divided by zero"};
}
return .{Ok=a / b};
}
main :: () {
println(fold(i32.[1, 2, 3, 4, 5], 0, add)); // Ok(15)
println(fold(i32.[120, 2, 3, 0, 6], 0, div)); // Err("divided by zero")
}
forward_err() の戻り値をどこにも代入しなくても早期リターンするため、バリデーションチェックのような使い方もできます。
some_validation_check(foo)->forward_err(); // エラーが発生したら早期リターン
このほかにも Result には便利なメソッドが用意されています。詳しくは公式リファレンスをご覧ください。
Resultの注意点
returnを書かないとエラーのデフォルト値が返る
return を書き忘れた場合、暗黙的に エラーのデフォルト値 (Result.{Err=.{}}) を返してしまいます。忘れずにreturnしましょう。
f :: () -> Result(i32, str) {
// returnし忘れた!
}
main :: () {
println(f()); // Err("")
}
どうやって実現している?
最後に、一介のメソッドにすぎない forward_err() がなぜ関数からの早期リターン可能かを見ていきます。
Onyxではマクロを使うことができ、 forward_err() はマクロで実装されています3。
forward_err :: macro (r: Result($T, $E)) -> T {
switch res := r; res {
case .Ok as v do return v;
// `return .{ Err = v }` というASTをコンパイル時に返すため、早期リターンが可能
case .Err as v do return return .{ Err = v };
}
}
forward_err は「result型の結果をパターンマッチし、エラーだった場合早期リターンする」というコードブロックを返します。このマクロがコンパイル時に評価されることで、以下のコードと等価な処理が得られていたというわけです。
value := foo()->forward_err();
res := foo();
valut: T;
switch res {
case .Ok as v {
valut = v;
}
case .Err as v {
return .{Err=v};
}
}
おわりに
以上、エラーハンドリングをシンプルに書く方法の紹介でした。こういう痒い所に手が届く仕組みは開発体験が上がるので嬉しいです。マクロがある言語ならではの仕組みですが、次に言語を作るときはせひ パク 参考にしたいと思います。