オフィスアワーがそろそろ始まるよ!()

エラー型

Zenにはエラー専用で使用するエラー型が存在します。プログラマは、関数の返り値型がエラー型を返すかどうかを見るだけで、その関数の処理が失敗する可能性があるかどうか、を認識できます。

Zenコンパイラは、このエラー専用型に対して様々な検査を行い、エラーを適切に処理するようにプログラマを支援します。

エラー型の定義

エラー型の定義は、列挙型と似ています。const 型名 = error { エラー種別, ... };でエラー型を定義します。

文字列から整数値に変換する処理を考えてみます。例えば、文字列"1234"を入力すると、u32型の1234に変換します。考えられるエラーは、次のものがあります。

  • 入力文字列に数字以外が含まれている
  • 入力文字列が空である
  • 変換結果がu32の範囲を超える

これを表現するエラー型は、次のように定義できます。

examples/ch03-user-defined-types/src/error.zen:4:8

const IntParserError = error {
   InvalidInputString,
   StringIsEmpty,
   ValueOutOfRange,
};

定義したエラー型は、次に説明するエラー共用体という形で関数の戻り値型とします。

エラー共用体

エラー共用体は、エラー型と通常の型 (u32など) とを1つの型として扱えるようにします。エラー共用体は!演算子を使い、エラー型!T (Tは任意の型) で宣言します。エラー共用体は、実用上は関数の戻り値型として利用することがほとんどです。

さきほど定義したIntParserErroru32とのエラー共用体を返す関数は、次のように定義できます。

fn parseU32(str: []const u8) IntParserError!u32 {
    // ...
};

エラー共用体は、ある時には1つのヴァリアントとして振る舞うので、parseU32関数はIntParserErroru32を返す関数である、と関数シグネチャから判断できます。このようにエラー共用体を戻り値型として持つ関数は、内部の処理が失敗する可能性があることを教えてくれます。

parseU32関数の実装は、次の通りです。文字列を1桁ずつ順番に読み取り、数値に変換します。

examples/ch03-user-defined-types/src/error.zen:10:33

fn parseU32(str: []const u8) IntParserError!u32 {
    if (str.len == 0) {
        return IntParserError.StringIsEmpty;
    }

    var result: u32 = 0;
    for (str) |c| {
        const digit = switch (c) {
            '0' ... '9' => c - '0',
            else => return IntParserError.InvalidInputString,
        };

        // result *= 10
        if (@mulWithOverflow(u32, result, 10, &result)) {
            return IntParserError.ValueOutOfRange;
        }

        // result += digit
        if (@addWithOverflow(u32, result, digit, &result)) {
            return IntParserError.ValueOutOfRange;
        }
    }
    return result;
}

IntParserError (関数の上から4番目までのreturn) かu32 (関数の最後のreturn) の値を返していることがわかります。それぞれのIntParserErroru32は、IntParserError!u32のエラー共用体型へ、暗黙の型変換が行われています。

エラー共用体のアンラップ

エラー型!Tのエラー共用体に格納されたエラー型もしくはTの値を使うためには、エラー共用体からアンラップしなければなりません。エラー共用体をアンラップする方法を説明します。

if

ifの条件式でエラー共用体インスタンスがエラー型かどうかを検査した上で、アンラップすることができます。if (エラー共用体インスタンス) |Tの値| { エラーでない時の処理 } else |エラー型の値 | { エラー処理 }という構文を使います。elseでエラー型を必ず処理しなければならないことに注意して下さい。

examples/ch03-user-defined-types/src/error.zen:35:51

test "unwrap with if" {
    // `u32`が格納されている場合
    var result: IntParserError!u32 = 42;
    if (result) |value| {
        ok(value == 42);
    } else |err| {
        // do nothing
    }

    // `IntParserError`が格納されている場合
    result = IntParserError.ValueOutOfRange;
    if (result) |value| {
        // do nothing
    } else |err| {
        ok(err == IntParserError.ValueOutOfRange);
    }
}

while

ifと同様に、whileにもエラー共用体インスタンスがエラー型かどうか検査した上で、アンラップする構文が用意されています。while (エラー共用体インスタンス) |Tの値| { エラーでない時の処理 } else |エラー型の値 | { エラー処理 }という構文になります。

catch

エラー共用体に利用できる専用の演算子として、catch演算子が用意されています。catch演算子は、2つの構文を提供しています。

  1. エラー共用体インスタンス catch 式
  2. エラー共用体インスタンス catch |エラー型の値| 式

エラー共用体インスタンスの型がエラー型!Tの場合、式はTの値もしくはnoreturn型である必要があります。

1つ目の構文では、式はTのデフォルト値を与える利用方法が多いです。

examples/ch03-user-defined-types/src/error.zen:53:60

test "unwrap with catch" {
    var result = parseU32("1234") catch 0;
    ok(@typeOf(result) == u32);
    ok(result == 1234);
    
    result = parseU32("invalid") catch 0;
    ok(result == 0);
}

parseU32の戻り値がエラー型の時のみ、デフォルト値として0が格納されます。resultの型はIntParserError!u32をアンラップしたu32となります。

式の部分は、noreturn型も許されるため、次のように、returnunreachableを書くこともできます。

examples/ch03-user-defined-types/src/error.zen:62:64

// エラーの場合、関数から戻る
test "expression of catch allows return" {
    var result = parseU32("invalid") catch return;
}

エラー共用体インスタンスが、絶対にエラー型にならないことが明らかな場合、catch unreachableとすることができます。

examples/ch03-user-defined-types/src/error.zen:66:68

test "result will never be an error" {
    var result = parseU32("1234") catch unreachable;
}

エラー型が格納されている場合に、デフォルト値を与える以外の処理がしたい場合、2つ目の構文を使います。

examples/ch03-user-defined-types/src/error.zen:70:76

test "handle error with catch" {
    var result = parseU32("invalid") catch |err| {
        std.debug.warn("display error: {}\n", err);
        std.debug.warn("abort!\n");
        return;
    };
}
display error: error.InvalidInputString
abort!

catch |err| { ... }{}ブロックは、型Tnoreturn型でなければならいことに注意して下さい。

try

エラー発生時の処理を、上位関数にそのまま伝播したい場合があります。その場合、tryを使うことができます。構文はtry エラー共用体インスタンス;です。

examples/ch03-user-defined-types/src/error.zen:78:87

fn tryParseU32(str: []const u8) IntParserError!void {
    var result = try parseU32(str);
    // `result` (u32) を使って何かやる
}

test "try" {
    var result = tryParseU32("invalid") catch |err| {
        ok(err == IntParserError.InvalidInputString);
    };
}

try エラー共用体インスタンス;エラー共用体インスタンス catch |err| return err;のシンタックスシュガーです。

エラー型の型変換

エラー型には、2つの暗黙の型変換が存在します。エラー型同士の型変換と、エラー型からエラー共用体への型変換です。

エラー型同士の暗黙の型変換は、サブセットからスーパーセットに変換する場合のみ、発生します。

例えば、次のSupersetErrorSubsetErrorがあるとします。

examples/ch03-user-defined-types/src/error_convert.zen:4:13

const SupersetError = error {
    A,
    B,
    C,
};

const SubsetError = error {
    A,
    B,
};

このようにスーパーセットとサブセットの関係にあるエラー型は、サブセットからスーパーセットへの暗黙の型変換が可能です。

examples/ch03-user-defined-types/src/error_convert.zen:15:18

test "implicit cast from subset to superset" {
    const err: SupersetError = SubsetError.A;
    ok(err == SupersetError.A);
}

次の、スーパーセットからサブセットへの変換は、コンパイルエラーになります。

    const err: SubsetError = SupersetError.A;

次のNotSubsetErrorも、SupersetErrorのサブセットになっていません。

const NotSubsetError = error {
    A,
    D,
};

そのため、次のコードはコンパイルエラーになります。

    const err: SupersetError = NotSubsetError.A;
error[E02046]: expected 'SupersetError', found 'NotSubsetError'
    const err: SupersetError = NotSubsetError.A;
                                             ~
note[E00021]: 'error.A' not a member of destination error set 'SupersetError'
    A,
    ~

エラー種別を1つだけ持つエラー型の宣言

エラー種別を1つだけ持つエラー型の値を生成する場合、専用のシンタックスシュガーが用意されています。error.エラー種別という構文で、error { エラー種別 }.エラー種別と等価になります。

examples/ch03-user-defined-types/src/error_convert.zen:25:29

test "syntax sugar for error declaration" {
    const err1 = error.AnError;
    const err2 = error { AnError }.AnError;
    ok(err1 == err2);
}

このシンタックスシュガーと、エラー型がスーパーセットへの暗黙の型変換を許すこと、とを組み合わることで、エラー型.エラー種別の代わりに、error.エラー種別の省略記法が使用できます。

examples/ch03-user-defined-types/src/error_convert.zen:31:39

fn implicitConversion() IntParserError!void {
    return error.InvalidInputString;
}

test "implicit conversion from syntax sugar" {
    implicitConversion() catch |err| {
        ok(err == IntParserError.InvalidInputString);
    };
}

switch

switchでエラー型のエラー種別ごとのハンドラを書くことができます。次のコードでは、エラー種別ごとにことなる警告メッセージを出力しています。

examples/ch03-user-defined-types/src/error.zen:105:117

fn handlerIntParserError(err: IntParserError) void {
    switch (err) {
        error.InvalidInputString => { 
            std.debug.warn("valid input characters are [0-9]\n");
        },
        error.StringIsEmpty => {
            std.debug.warn("string must have one or more length\n");
        },
        error.ValueOutOfRange => {
            std.debug.warn("valid value range is between 0 to 2^32\n");
        },
    }
}

エラー型のマージ

任意のエラー型をマージすることができます。エラー型のマージには||演算子を使用します。IntParserErrorKeywordParserErrorをマージして、ParserErrorを定義します。

examples/ch03-user-defined-types/src/error_convert.zen:41:52

const IntParserError = error {
    InvalidInputString,
    StringIsEmpty,
    ValueOutOfRange,
};

const KeywordParserError = error {
    NoSuchKeyWord,
    StringIsEmpty,
};

const ParserError = IntParserError || KeywordParserError;

ParserErrorは2つのエラー型のエラー種別を合わせたエラー型になります。

examples/ch03-user-defined-types/src/error_convert.zen:54:64

test "merge error sets" {
    const err: ParserError = error.StringIsEmpty;
    switch (err) {
        error.InvalidInputString,
        error.ValueOutOfRange,
        error.NoSuchKeyWord
            => ok(false),
        error.StringIsEmpty
            => ok(true),
    }
}

エラー型の推論

関数の戻り値型におけるエラー型を推論させることができます。エラー型を推論させる場合、!Tでエラー共用体の型を宣言します。

examples/ch03-user-defined-types/src/error_convert.zen:66:68

fn inferredError() !void {
    return error.StringIsEmpty;
}

inferredError関数の戻り値型は、!Tです。この関数は戻り値型がジェネリックな関数になります。

examples/ch03-user-defined-types/src/error_convert.zen:70:81

test "error type inference" {
    inferredError() catch |err| {
        ok(err == IntParserError.StringIsEmpty);
    };

    inferredError() catch |err| {
        ok(err == KeywordParserError.StringIsEmpty);
    };

    inferredError() catch |err| {
        ok(err == ParserError.StringIsEmpty);
    };
}

関数の取り扱いがトリッキーになるため、可能な場合は、明示的にエラー型を記述することをお勧めします。

anyerror

anyerrorはコンパイル時に判明する全てのエラー型を内包するエラー型です。anyerrorは全てのエラー型のスーパーセットです。そのため、全てのエラー型は、anyerrorに暗黙の型変換が可能です。

☰ 人の生きた証は永遠に残るよう ☰
Copyright © 2018-2019 connectFree Corporation. All rights reserved.
Zen, the Zen three-circles logo and The Zen Programming Language are trademarks of connectFree corporation in Japan and other countries.