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

今回も[Onyx](https://onyxlang.io/)の記事です。（過去の記事は[こちら](https://qiita.com/search?sort=&q=user%3Asyuparn+tag%3Aonyx)）

# TL; DR

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

# はじめに

Onyxでは `Optional` や `Result` 型により失敗しうる処理を表現可能です。大域脱出が無い場合エラーハンドリングが煩雑になりがちですが、Onyxには便利なシンタックスシュガーが用意されていました。

# OptionalとResult
本題に入る前に `Optional`, `Result` 型について見ていきます。馴染みのある方は次の章まで読み飛ばしてください。

## Optional
空である可能性がある値を表します。Onyxにはポインタ型もあるのですが、 `null` と違いパターンマッチで静的にハンドリングできるので安全です[^pattern]。

```onyx
// 定義
println(Optional.empty(str)); // None
println(Optional.make("foo")); // Some("foo")
```

```onyx
// 使用例
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` は「値もしくはエラー」を表す型です。失敗しうる操作の戻り値に使用されます。こちらもエラーハンドリングはパターンマッチで行えます。

```onyx
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` を早期リターンする

という風にはたらきます。

```onyx
// 本文で?を使う場合、戻り値型を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
}
```

## ??でガード
値が無い場合に早期リターンではなくデフォルト値を返したい場合は `??` を使用します。

```onyx
opt := Optional.empty(str);
println(opt ?? "default_value"); // default_value
```

## Result#forward_errで早期リターン
`Result` もエラーハンドリングを簡潔にすることが可能です[^result_err]。

戻り値が Result型の関数内で Result型の式に対して `forward_err()` メソッドを呼び出すと

- 値が存在する場合は中身が取り出される
- 存在しない場合はエラーを早期リターンする

という風にはたらきます。

```onyx
// 説明のために実装しているが、標準ライブラリにも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()` の戻り値をどこにも代入しなくても早期リターンするため、バリデーションチェックのような使い方もできます。

```onyx
some_validation_check(foo)->forward_err(); // エラーが発生したら早期リターン
```

このほかにも `Result` には便利なメソッドが用意されています。詳しくは公式リファレンスをご覧ください。

https://docs.onyxlang.io/packages/core

### Resultの注意点
#### returnを書かないとエラーのデフォルト値が返る

`return` を書き忘れた場合、暗黙的に **エラーのデフォルト値** (`Result.{Err=.{}}`) を返してしまいます。忘れずにreturnしましょう。

```onyx
f :: () -> Result(i32, str) {
    // returnし忘れた！
}

main :: () {
    println(f()); // Err("")
}
```

### どうやって実現している？
最後に、一介のメソッドにすぎない `forward_err()` がなぜ関数からの早期リターン可能かを見ていきます。
Onyxではマクロを使うことができ、 `forward_err()` はマクロで実装されています[^macro]。

```onyx: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 };
        }
    }
```

https://github.com/onyx-lang/onyx/blob/master/core/container/result.onyx#L143C1-L148C6

`forward_err` は「result型の結果をパターンマッチし、エラーだった場合早期リターンする」というコードブロックを返します。このマクロがコンパイル時に評価されることで、以下のコードと等価な処理が得られていたというわけです。

```onyx:これが
value := foo()->forward_err();
```

```onyx:こうなる（イメージ）
res := foo();
valut: T;
switch res {
    case .Ok as v {
        valut = v;
    }
    case .Err as v {
        return .{Err=v};
    }
}
```

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

[^pattern]: `Optional` や `Result` の定義に使用している `union` の仕様です。
[^result_err]: 正確にはシンタックスシュガーではなくマクロですが
[^macro]: Onyxのマクロはいわゆる「真のマクロ」（文字列置換ではなくAST置換）です。