0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【Onyx】?とforward_errでスマートにエラーハンドリング

Posted at

今回もOnyxの記事です。(過去の記事はこちら

TL; DR

  • Optional? を付ける or Result->forward_err() を呼ぶと、失敗した場合に早期returnしてくれる

はじめに

Onyxでは OptionalResult 型により失敗しうる処理を表現可能です。大域脱出が無い場合エラーハンドリングが煩雑になりがちですが、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

core/container/result.onyx
    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};
    }
}

おわりに

以上、エラーハンドリングをシンプルに書く方法の紹介でした。こういう痒い所に手が届く仕組みは開発体験が上がるので嬉しいです。マクロがある言語ならではの仕組みですが、次に言語を作るときはせひ パク 参考にしたいと思います。

  1. OptionalResult の定義に使用している union の仕様です。

  2. 正確にはシンタックスシュガーではなくマクロですが

  3. Onyxのマクロはいわゆる「真のマクロ」(文字列置換ではなくAST置換)です。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?