自作インタプリタ「Pad」にtry-catch文(例外処理)を実装した

316, 2021-08-31

目次

自作インタプリタ「Pad」にtry-catch文(例外処理)を実装した

5年ぐらい前から「Pad」というインタプリタを作ってます。

つい先日、このPadに例外処理を実装しました。
その実装について紹介します。
具体的には↓を見ていきます。

  • プログラミング言語のエラーハンドリング

  • 例外処理とは

  • Padの例外処理の内容

  • Padの例外処理の実装

  • 実装に関して大変だったこと

プログラミング言語のエラーハンドリング

プログラミング言語とエラーは切っても切り離せないものです。
プログラミングでは必ずエラーが発生します。
それはファイルの入出力のエラーだったり、メモリ不足のエラーだったり、色々です。

プログラミング言語の設計ではこのエラー処理のデザインを組み込んでおく必要があります。
つまり、プログラミングでエラーが発生したら開発者にどのようにエラーを処理させるかという問題です。

この問題のアプローチは大きく分けて2つあると思います。
1つが返り値(戻り値)によるエラーハンドリングです。
これはC言語やGo言語などで見られるエラーハンドリングで、いにしえの時代から存在します。

もう1つが例外によるエラーハンドリングです。
エラーになったら例外を発生させ、例外を投げる(throw)という感じです。

オブジェクト指向色の強いプログラミング言語では例外が実装されていることが多いです。
C++, Python, Javaなど。

Padは当初、返り値によるエラーハンドリングを行っていましたが、途中で設計を変更して例外によるエラーハンドリングに切り替えました。
理由はいろいろありますが、これは後述したいと思います。

例外処理とは

例外処理とは例外を投げて、それをキャッチしてエラーをハンドリングするという仕組みです。
例外を投げることをthrowする、キャッチすることをcatchすると言います。
Pythonではthrowはraise, キャッチはexceptになってます。

たとえばC++の例外処理は↓のような感じです。

#include <iostream>
#include <stdexcept>

int main() {
    try {
        throw std::runtime_error("failed");
    } catch (const std::runtime_error &e) {
        std::cout << e.what() << std::endl;
    }
    return 0;
}

tryで始まるブロックの中で例外が発生すると、catchが実行されます。
↑の例ではtryの中で例外std::runtime_errorを投げています。
↑のコードをコンパイルして実行すると端末にはfailedと表示されます。

例外が発生すると、コードを飛び越えてcatchのところまでやってきます。
その様子を「ジャンプする」などと表現することがあります。
(Padの実装では実際にはジャンプはしていません)。

Padのエラーハンドリングを例外処理にした理由

Padは当初、返り値によるエラーハンドリングを行っていました。
しかし、これを変更して例外処理を実装しました。
なぜかというと、例外処理を実装したほうが組み込み関数などのインターフェースがシンプルになるからです。

従来のPadのエラーハンドリン方法は例えば↓です。

{@
    def func():
        return nil, "error!"
    end

    result, err = func()
    if err != nil:
        puts(err)
    end
@}

このエラーハンドリングはGoと同じです。
返り値の多値返却で、エラーを表すオブジェクトを返却して、そのオブジェクトがnilでなければエラーハンドリングを行います。
この設計は多値返却が実装できれば使えるので、Padの実装初期から存在していました。

なぜこのエラーハンドリングをやめて例外処理にしたのかと言うと、多値返却だと組み込み関数などのインターフェースが複雑になるからです。
たとえばPadにはord()という組み込み関数があります。
この関数は引数の文字列をコードポイントに変換する関数です。

従来のord()は↓のような感じでした。

{@
    cp, err = ord("a")
@}

エラーハンドリングが多値へんきゃのため、↑のようなインターフェースになっています。
しかし、これを実装して使った感じだと、私の感覚ではあまりコードが美しくなりませんでした。
たとえばエラーをはぶいて結果を得たい場合などは↓のようなコードを書く必要があります。

{@
    ord("a")[0]
@}

Padの多値返却はArray(配列)で返ってきます。そのため↑のように返り値のインデックスを参照すると、結果だけを取ることができます。
しかし、↑のようなコードは私の感覚ではあまり美しいとは思えませんでした。
どちらかというと、↓のようなコードのほうが美しい気がします。

{@
    try:
        result = ord("a")
    catch ValueError as e:
        puts(e.what())
    end
@}

↑のように例外処理を実装すれば、エラーのハンドリングをあえてしない場合はtry-catch文を書かなければそれですみます。
多値返却の場合はエラーを無視する場合はさっきのようなコードを書く必要があります。

これは言語の設計思想によります。
たとえばGoのようにエラーの存在を明確にしたい場合は多値返却のような設計が向いてます。
なぜならエラーを無視する場合は↓のようなコードにする必要があるからです。

{@
    result, _ = ord("a")
@}

↑のコードは明確なエラーの無視になっていて、例外に比べるとエラーの存在がハッキリしています。
例外の場合はtry-catch文を書かなければエラーの存在がわかりません。

エラーを逐一把握したい人はGoのようなエラーハンドリングが向いていて、エラーは必要な時でいいという人は例外処理が向いていると言えます。
私は後者だったようなので例外処理を実装しました。

Padの例外処理の内容

Padの例外処理は↓のようなコードになります。

{@
    try:
        throw Exception("error")
    catch (ValueError, TypeError) as e:
        puts(e.what())
    catch Exception as e:
        puts(e.what())
    end
@}

Padは全般的にPythonから強い影響を受けています。
そのため例外処理の実装でもPythonを参考しています。

組み込みの例外型は複数あって、Exception, ValueError, TypeErrorなどはその一部です。
Exceptionなどは構造体の名前です。Exceptionはベースとなる例外構造体で、ValueErrorTypeErrorExceptionから派生しています。

独自例外を投げたい場合は↓のようにします。

{@
    struct MyError:
        extract(Exception)
    end

    try:
        throw MyError("error")
    catch MyError as e:
        puts(e.what())
    end
@}

↑のように独自例外の構造体を作る場合は、Exceptionextract()します。
extract()は引数のオブジェクトのコンテキストを、その場にぶちまける組み込み関数です。
こうすることでMyErrorExceptionの属性を得ることができます。

Padには継承は存在しませんが、構造体の拡張などは↑のようにextract()を使うと簡単に行えます。
Exceptionはメソッドwhat()を持っていて、これは属性messageを返すメソッドです。
属性messageには↑の場合errorという文字列が保存されているので、画面には「error」と表示されます。

Padの例外処理の実装

Padの例外処理の実装ですが、簡単に言うとエラースタックをハンドリングしています。
エラースタックとはPadがエラーが発生したときにプッシュするスタックのことです。
スタックにはエラーメッセージなどが保存されていて、エラーが発生した場合はこのスタックがトレースされて画面にスタックトレースが出力されます。

例外処理の実装にともなって、このエラースタックの要素に例外を表す変数を追加しました。これはPadExc型の整数です。
Padはエラーが発生したら、このエラースタックにエラーメッセージとPadExc型の値をプッシュします。

throw文は強制的にエラースタックに要素をプッシュする文です。
エラースタックにエラー(例外)がプッシュされると、Padはエラーを持つようになり、各処理を中断します。
最終的にエラーは処理のルートまで中断されて、画面にスタックトレースが出力されます。

try文の中でエラーが発生した場合、try文はcatchを呼び出します。
そしてcatchは指定されている例外が発生しているかどうか調べて、発生していた場合はcatchの中の処理を実行します。
catchが実行される場合は現在のコンテキストのエラースタックは一時的にクリアされます。
catchの中でエラーが発生した場合はふたたび例外が送出されます。

実装に関して大変だったこと

Padの例外処理の実装で大変だったのは、バグのフィックスです。
実装自体はそれほど大変ではなかったのですが、途中でバグが生まれて、これをフィックスするのが大変でした。
このバグはコピーに関するバグで、コピー漏れ(コードの書き忘れ)がありバグが生まれていました。
このバグをフィックスしたときの達成感はすばらしいものでした。

Padの設計当初からエラースタックを実装していたのは良かったポイントです。
このエラースタックを実装していなかったら、例外処理の実装はもっと大変なものになっていたと思います。

転ばぬ先の杖

転びまくってたけど

おわりに

今回は自作インタプリタ「Pad」の例外処理について詳しく見てみました。
インタプリタの実装で例外処理を実装する場合に何かの参考になれば幸いです。

例外でジャンプしよう

スタックをパンパンにするぞ