eval 関数を評価する

昨日作った簡単な grep コマンドの中で eval と $@ による例外処理を勉強しましたが、eval 関数について詳しく勉強してみます。
僕の知っている Lightweight LanguageJavaScriptPythonRubyPHP など)のほとんど(全部?)に「eval」関数が存在します。これらに共通しているのは「文字列として渡したコードを評価する」という機能ですが、Perl の eval 関数もやはり文字列を渡すとコードとして評価してくれます。しかも、文字列にとどまらずブロックを渡す事も可能です。
eval 関数の返り値は、サブルーチンを書く時と同じようにコードの中で return を使って返した値か、コードの最後の式の評価結果が返り値になります。返り値は eval 関数を呼び出したコンテキストで評価されます。例えば配列への代入の右辺として eval 関数を呼び出した場合は、リストコンテキストで評価されるわけですね。(^_^)


実際に使ってみます。

use strict;
 
eval 'print "Hello, eval!\n"';
 
my $code = join "*", (1..10);
my $fact = eval $code;
 
eval { print "10! = $fact\n"; };

1つ目の eval では文字列として渡した print 文を実行してもらいます。
2つ目の eval では $code をコードとして評価した返り値を $fact に代入しています。$code には「1*2*3*4*5*6*7*8*9*10」という文字列が入っています。評価されれば 10 の階乗(10!)になりますね。(^_^)
3つ目の eval ではブロックを渡しています。ここで注目したいのは eval に渡しているブロックの中で、“外側”のレキシカル変数である $fact を参照している点です。ブロックは eval 関数が置かれているスコープの中で実行されるので、外側のレキシカル変数も参照できます。
実行結果は以下の通りです。

Hello, eval!
10! = 3628800

評価できています。文字列からの階乗の計算も上手く動いていますね。


ちなみに、文字列を渡すより、ブロックを渡した方がかなり効率的との事。でも、ブロックを渡す場合は先ほどの階乗のように、文字列を作ってから評価するような事はできません。適材適所ですね。(^_^)


そして eval 関数を使うもう一つの理由が「例外処理」です。「例外処理」とは Wikipedia によると以下のように説明されています。

例外処理(れいがいしょり)とは、プログラムがある処理を実行している途中で、なんらかの異常が発生した場合に、現在の処理を中断(中止)して、別の処理を行うこと。その際に発生した異常のことを例外と呼ぶ。

例えば、前回のエントリでは「ファイルの open に失敗した場合」を「例外」としていました。ファイルを開けなければ、それ以降の処理は実行できませんので、そこで die 関数をエラーメッセージ付きで呼び出していました。

open TARGET, $fn or die "Can't open $fn: $!";

何も Perl の例外は die 関数だけというわけではなく、例えば「構文エラー*1」や「モジュールなどの読み込みエラー」「0 による除算」なども例外の一種です。(実行時エラーは全て例外扱い?)
通常は、このような例外が発生した場合、その時点でプログラムの実行がストップしてしまいます。でも eval 関数で評価されるコードの中で例外が発生した場合は、eval 関数を抜け出すだけで、プログラムの実行は止まりません。

$fn = "nonexist.file";
open FH, $fn or die "Can't open $fn: $!";           # ERROR! → 実行が停止
eval { open FH, $fn or die "Can't open $fn: $!"; }  # ERROR! → だけど実行は停止しない 

eval 関数の中で例外が発生した場合、eval 関数は undef を返し、発生した例外の内容を「$@」という特殊変数にセットします。エラーが起こらなかった場合 $@ は空文字列となるので、eval の後に $@ をチェックすれば、例外が発生したかどうかわかります。

$fn = "nonexist.file";
eval {
	open FH, $fn or die "Can't open $fn: $!";
}
if ($@) {
	print STDERR "$@\n";  # Can't open nonexist.file: ...
} 

C++Java の場合の例外処理は try ... catch で行いますが、eval が try に、if ($@) が catch に相当するわけですね。ただし try ... catch とは違い、補足する例外の種類は限定できず、どんな例外でもキャッチしてしまいます。
つまり、eval するコード内で発生した例外が「本当に想定外のエラー」だったとしてもキャッチしてしまうので、注意が必要です。eval する範囲はなるべく狭くした方がいいですね。(^_^)

*1:構文エラーは文字列を eval した時のみ補足できます。ブロックを渡す場合、中のコードがコンパイルされるからです。