Perlで例外オブジェクトをeval & catch

ひさびさにPerlベストプラクティスネタ。

Perlで例外処理というと、evalして$EVAL_ERROR(= $@)の文字列を判定するしかないと思っていたのだけれど、ちゃんと例外オブジェクト使う方法があった。やっぱりなんでもありだなPerlは。

PBPによると、例外オブジェクトは自分で作ることもできるのだけれど、いくつも注意しなければならないポイントがあるようだ。

  • 文字列変換(q{""})のオーバーロード
  • 例外をキャッチしたかを返すcaughtメソッド
  • エラーメッセージの出力先の変更

このあたりが正しく実装できていなければだめ。しかしながら、CPANにちょっとお伺いをたてるだけで、例外用のException::Classといういかにも便利そうなモジュールを利用できるらしい。

このException::Classを利用して、Perlの例外オブジェクトを定義してみた。

use warnings;
use strict;
use English qw( -no_match_vars );
use IO::Prompt;

use Exception::Class (
    'X::TooLongString' => {
        fields => ['value'],
    }
);

sub X::TooLongString::full_message {
    my ($self) = @_;
    return "String " . $self->value . " is too long";
}

フィールドにvalueを持つ例外クラス、X::TooLongStringが定義できた。

このクラスをつかって例外を送出してみよう。次のサブルーチンではユーザから入力を得るのだけれども、あまりにそれが長過ぎた場合には例外を送出する。

sub process_string_from_user {
    # ユーザから値を得る
    # use IO::Promptが必要
    my $user_str = prompt 'Give me long string: ';

    # 文字列の長さが100文字以上なら例外を送出
    X::TooLongString->throw( value => $user_str )
        if (length $user_str > 100);

    # 100文字以上の文字列を受け付けないデバイスを使った
    # 処理がはじまる
}

Exception::Classでつくったクラスはthrowメソッドをもっているので、そのメソッッドを利用すれば、簡単に例外を投げることができる。

このコードをつかって実際に例外を捕捉するのには、次のようなコードを書いてみた。

# try
eval {
    process_string_from_user();
};

# catch
if ($EVAL_ERROR) {
    if ( X::TooLongString->caught ) {
        my $user_str = $EVAL_ERROR->value;
        print qq{"$user_str" is toooooooo loooong.\nSpilled data was lost.. Never mind.\n}
    }
}

evalでキャッチされた例外は$EVAL_ERROR (= $@)に入る。そのとき、もし$EVAL_ERRORがX::TooLongStringオブジェクトであれば、X::TooLongStringのメソッドcaughtはTrueになる。

というわけで、実際にX::TooLongString例外を起こしてみよう。

$ perl exception_class.pl
Give me long string: hakobe
$ perl exception_class.pl
Give me long string: toooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooolooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong
"toooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooolooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong" is toooooooo loooong.
Spilled data was lost.. Never mind.

ちょうながい文字列を入力すると確かに例外が送出された。んでもって、$EVAL_ERROR->valueをすることで発生した例外オブジェクトから得た、文字列データを利用することもできた。

例外オブジェクトを利用することで、より直感的に例外を扱えるようになった。再利用しにくい文字列処理やパッケージ変数(= グローバル変数)にもなるべく会わなくてすむ。

しかし、例外で処理すべき物とifで処理すべき物の違いが直感的にわかんないんだよな。ifで処理というとエラー処理かしらね。エラーと例外は違う物らしく、

  • 例外: プログラムが実行を進めるなかで起こり得ないもの -> 起こったらどうしようもないもの?
  • エラー: そのプログラム内でどうにか処理できるもの?

うーん?