bit

Perl の eval で Java の try〜catch を実現したい場合に注意すること

Java などの try〜catch 構文を Perl で実現するには、eval() 関数を使えばよいというブログを見かける。

間違ってはいないのであるが、陥りやすい罠がある。プログラムを組む上で、エラー処理は非常に重要なので、ここに記載しておくことにする。

例として、Java でファイルを一行ごとに読み込むプログラムを考える (try〜catch と言いながら、catch がないけどイメージは伝わるはず)。

  BufferedReader fromFile = null;
  try {
    fromFile = new BufferedReader(new FileReader(..));
    while(true) {
      String line = fromFile.readLine();
      if (line == null) {
        break;
      }
       ..(省略)..
    }
  } finally {
    if (fromFile != null) {
      fromFile.close();
    }
  }

例えば存在しないファイルを開こうとした場合、ファイルを開く部分、つまり new FileReader(..) の部分で例外が発生し、処理は finally に移行する。

この挙動を期待して、そのまま Perl で書いたプログラムが以下になる。eval() もちゃんと使っている。

  my $from_file = undef;
  eval {
    open $from_file, '<', $file_path;
    while(my $line = <$from_file>) {
      ..(省略)..
    }
    close $from_file;
  };

  if ($@) {
    my $tmpdie = $@;
    print STDERR "ERROR: $@\n";
    if (defined $in_fh) {
      close $from_file;
    }
    die $tmpdie;
  }

Java に慣れきっていると、open() でエラーになって eval() を抜けてほしいものだが、そうはならない。
そのまま突き進んで、<$from_file> で偽が返るので while() には入らず、さらに close() も突き抜ける。eval() 自体の返り値は偽になるが、それを変数で受けている訳ではないので、$@ は空文字(=偽)のまま。従って if() の中のエラー処理もスキップされる。

じゃあどうすればいいのかというと、Perl の基本に立ち返る。
つまりちゃんと die() する。

  my $from_file = undef;
  eval {
    open $from_file, '<', $file_path or die "open $file_path";
    while(my $line = <$from_file> or die 'read line') {
      ..(省略)..
    }
    close $from_file or die 'close file handler';
  };

  if ($@) {
    my $saved_err = $@;
    print STDERR "ERROR: $@\n";
    if (defined $in_fh) {
      close $from_file;
    }
    die $saved_err;
  }

全部 die() しなきゃいけないのか、とうんざりした人は、ちゃんとモジュール化する (または Perl をやめる:-P)。

package FileReader;
sub fr_open {
  ..(省略)..
  open $from_file, '<', $file_path or die "open $file_path";
  return $from_file;
}

sub fr_read_line {
  my $fh = $_[0];
  return <$fh> or die 'read line';
}

sub fr_close {
  close $_[0] or die 'close file handler';
}

package ErrorHandler;
sub handle_error {
  ..(省略)..
}
1;
package Main;
use ...;
sub main {
  my $from_file = undef;
  eval {
    $from_file = FileReader::fr_open($file_path);
    while (my $line = FileReader::fr_read_line($from_file)) {
      ..(省略)..
    }
    FileReader::fr_close($from_file);
  };
  if ($@) {
    ErrorHandler::handle_error($@);
  }
}
main;

Exporter使うと、接頭辞がなくなってもう少しきれいに書ける。

追記(4/10):
コメントで教えていただいた autodie について調べてみた。便利だとは思ったが、正直なところ、ある一つの欠点が気になった。
それは、ビルトイン関数*1以外のサブルーティンに autodie を適用させる場合には、そのサブルーティン名を一つ一つ宣言しないといけないことである。
例えば File::Path::make_path 関数に autodie を適用する場合、以下のように書く。

use File::Path qw(make_path);
use autodie qw(make_path);

eval {
  my $path = undef;
  make_path($path);
};
if ($@ and $@->isa('autodie::exception')) {
  if ($@->matches('File::Path::make_path') { ... }
}

この例では関数が一つであるが、複数ある場合それらをすべて「use autodie qw(A B C...)」と宣言しないといけない。
今回の主題はエラーハンドリング漏れをいかに防ぐかということであり、その観点から言うと autodie は完全な回答にはならない。
もちろんビルトイン関数に対しては有用であるため、とりあえず宣言しておく、という使い方もない訳ではないが、今度はプログラマが書いたビルトイン関数への「or die」を横取りしてしまうことになる(動きとしては自然だが)。
つまるところ、どれがビルトイン関数でどれがそうでないのか、プログラマとレビューアが把握しなければならなくなる点が、この autodie の適用の難しいところである。

*1:autodie が文字通り自動的に適用される[]関数[]の一覧は、perldoc autodie に記載されている