M.Hiroi's Home Page

Linux Programming

お気楽 Perl プログラミング超入門

[ PrevPage | Perl | NextPage ]

eval と例外処理

最近のスクリプト言語は、プログラムの実行中に数式や別のプログラムを組み立て、それを実行することができるようになっています。これを「動的プログラミング」とか「実行時評価」と呼びます。C言語などのコンパイラでは実現するのが難しい [*1] 機能です。

Perl の場合、関数 eval [*2] を使って、実行時に数式やプログラムを組み立てて、それを評価することができます。また、Perl の eval はちょっと変わっていて、「例外処理」にも使用することができます。今回は eval と例外処理について説明します。

-- note ---------
[*1] やってやれないことはないのでしょうが、プログラムを実行するまでの時間 (コンパイルに時間がかかる) を考えると、実用的ではないでしょう。
[*2] eval は Lisp で生まれた関数です。eval は evaluate の略で「プログラムを評価する」という意味です。Python, Ruby, Tcl/Tk などでも eval は実装されています。

●eval の使い方

eval は与えられた文字列を Perl プログラムとして実行します。つまり、プログラムの実行中に文字列をコンパイルして、それを実行するわけです。簡単な使用例を示しましょう。

リスト ; eval の使用例 (sample2001.pl)

use strict;
use warnings;

my $a = eval '1 + 2 * 3 - 4';
print $a, "\n";

my $b = 10;
my $c = eval '$a / $b';
print $c, "\n";
$ perl sample2001.pl
3
0.3

eval に文字列 '1 + 2 * 3 - 4' に渡すと、'1 + 2 * 3 - 4' を Perl プログラムとして実行します。その結果、変数 $a に 3 がセットされます。次に、'$a / $b' を eval に渡します。文字列はシングルクォートで囲まれているので、変数展開されないことに注意してください。eval を実行するときにアクセスできる変数は、eval に与えるプログラムからでもアクセスできます。この場合、$a, $b の値は eval が実行される前に代入された値である 3 と 10 になるので、結果は 0.3 になります。

eval に渡す文字列には、複数の「文」を渡すことができます。

リスト : eval の使用例 (sample2002.pl)

use strict;
use warnings;

my $a = 0;
my $b = 0;
my $c = eval '$a = 10; $b = 20';
print "$a $b $c\n";
$ perl sample2002.pl
10 20 20

この場合、eval は 2 つの文 $a = 10 と $b = 20 を実行します。eval は最後に実行した文の結果を返すので、$b = 20 の実行結果である 20 が返り値となります。

もう少し複雑な例題を示しましょう。標準入力からプログラムを受け取り、それを eval で実行します。

リスト : 簡単な Perl シェル (sample2003.pl)

use strict;
use warnings;

while (<>) {
    my $r = eval;
    if ($@) {
        print $@, "\n";
    } else {
        print $r, "\n";
    }
}

行入力演算子を使って標準入力より 1 行読み込んで、それを eval で実行します。eval は引数が省略されると、特殊変数 $_ に格納された文字列をプログラムとして実行します。Perl はプログラムを中間コードにコンパイルしてから、それを実行します。したがって、eval を実行するときには、コンパイルエラーや実行時のエラーが発生する場合があります。エラーが発生すると、そのエラーメッセージが特殊変数 $@ に格納され、eval の返り値は未定義値になります。エラーがなければ、$@ の値は未定義値となります。

たったこれだけのプログラムでも、入力された文字列が Perl の文法を満たしていれば、それを実行することができるのです。このままでも次に示すように関数電卓として利用することができます。

$ perl sample2003.pl
1 + 2 * 3 - 4;
3
sin(0);
0
cos(0);
1
foreach (1 .. 10) { print $_ * 10; }
102030405060708090100

このプログラムを終了するときは CTRL-C か CTRL-D を押してください。

●パズル 小町算

それでは簡単な例題として数字のパズルを解いてみましょう。

[問題1] 小町算

1 から 9 までの数字を順番に並べ、間に + と - を補って 100 になる式を作ってください。

例:1 + 2 + 3 - 4 + 5 + 6 + 78 + 9 = 100

パズルの世界では、1 から 9 までの数字を 1 個ずつすべて使った数字を「小町数」といいます。たとえば、123456789 とか 321654987 のような数字です。「小町算」というものもあり、たとえば123+456+789 とか 321 * 654 + 987 のようなものです。問題1は小町算の中でも特に有名なパズルです。

Perl で解く場合、式は文字列で組み立てて、それを eval で計算した方が簡単です。プログラムは次のようになります。

リスト:小町算 (sample2004.pl)

use strict;
use warnings;

sub komachi {
    my ($n, $expr, $ans) = @_;
    if ($n == 10) {
        print "$expr = $ans\n" if eval($expr) == $ans;
    } else {
        foreach my $op (' + ', ' - ', '') {
             my $new_expr = $expr . $op . $n;
            komachi($n + 1, $new_expr, $ans);
        }
    }
}

# 実行
komachi(2, '1', 100);
komachi(2, '-1', 100);

数字の間に入れる演算子は ' + ' と ' - ' と '' です。演算子として空文字列 '' を入れるところがポイントです。本当の演算子ではありませんが、これで数式を組み立てるときに、12 とか 345 のように数字と数字を結合することができます。あとは komachi(2, '1', 100) と呼び出すだけです。それから、'1' のかわりに '-1' を与えれば、- 符号から始まる式でも求めることができます。

結果は次のようになりました。

$ perl sample2004.pl
1 + 2 + 3 - 4 + 5 + 6 + 78 + 9 = 100
1 + 2 + 34 - 5 + 67 - 8 + 9 = 100
1 + 23 - 4 + 5 + 6 + 78 - 9 = 100
1 + 23 - 4 + 56 + 7 + 8 + 9 = 100
12 + 3 + 4 + 5 - 6 - 7 + 89 = 100
12 + 3 - 4 + 5 + 67 + 8 + 9 = 100
12 - 3 - 4 + 5 - 6 + 7 + 89 = 100
123 + 4 - 5 + 67 - 89 = 100
123 + 45 - 67 + 8 - 9 = 100
123 - 4 - 5 - 6 - 7 + 8 - 9 = 100
123 - 45 - 67 + 89 = 100
-1 + 2 - 3 + 4 + 5 + 6 + 78 + 9 = 100

- 符号から始まる式を含めると、解は全部で 12 通りになります。

それでは、数字を逆順に並べたらどうなるでしょうか。

[問題2] 逆順の小町算

1 から 9 までの数字を逆順に並べ、間に + と - を補って 100 になる式を作ってください。

例:9 - 8 + 7 + 65 - 4 + 32 - 1 = 100

この問題も Perl を使って簡単にプログラムを作ることができます。次のリストを見てください。

リスト:逆順の小町算

use strict;
use warnings;

sub komachi_rev {
    my ($n, $expr, $ans) = @_;
    if ($n == 0) {
        print "$expr = $ans\n" if eval($expr) == $ans;
    } else {
        foreach my $op (' + ', ' - ', '') {
            my $new_expr = $expr . $op . $n;
            komachi_rev($n - 1, $new_expr, $ans);
        }
    }
}

komachi_rev(8, "9", 100);
komachi_rev(8, "-9", 100);

関数 komachi_rev を再帰呼び出しするとき、引数 $n の値を -1 します。これで数字を逆順に並べることができます。そして、$n の値が 0 になったら再帰呼び出しを停止して式の値を eval で計算します。とても簡単ですね。結果は次のようになります。

9 + 8 + 76 + 5 + 4 - 3 + 2 - 1 = 100
9 + 8 + 76 + 5 - 4 + 3 + 2 + 1 = 100
9 - 8 + 7 + 65 - 4 + 32 - 1 = 100
9 - 8 + 76 + 54 - 32 + 1 = 100
9 - 8 + 76 - 5 + 4 + 3 + 21 = 100
98 + 7 + 6 - 5 - 4 - 3 + 2 - 1 = 100
98 + 7 - 6 + 5 - 4 + 3 - 2 - 1 = 100
98 + 7 - 6 + 5 - 4 - 3 + 2 + 1 = 100
98 + 7 - 6 - 5 + 4 + 3 - 2 + 1 = 100
98 - 7 + 6 + 5 + 4 - 3 - 2 - 1 = 100
98 - 7 + 6 + 5 - 4 + 3 - 2 + 1 = 100
98 - 7 + 6 - 5 + 4 + 3 + 2 - 1 = 100
98 - 7 - 6 + 5 + 4 + 3 + 2 + 1 = 100
98 - 7 - 6 - 5 - 4 + 3 + 21 = 100
98 - 76 + 54 + 3 + 21 = 100
-9 + 8 + 7 + 65 - 4 + 32 + 1 = 100
-9 + 8 + 76 + 5 - 4 + 3 + 21 = 100
-9 - 8 + 76 - 5 + 43 + 2 + 1 = 100

解は全部で 18 通りあります。

●例外処理

次は「例外処理」について説明します。例外は「エラー処理」で使われることがほとんどなので、「例外=エラー処理」と考えてもらってもかまいません。最近は例外処理をサポートしているプログラミング言語が多くなりました。この中で Perl の例外処理はちょっと変わっています。

Perl の例外処理は eval を使います。eval は文字列が与えられると、それを Perl のプログラムとして評価します。ところが、文字列のかわりにブロック ({ } で囲まれたプログラム) を与えると、eval は例外処理の働きをするのです。具体的には、ブロックで発生したエラーを捕まえることができます。次のプログラムを見てください。

リスト : Perl の例外処理 (sample2005.pl)

use strict;
use warnings;

eval {
    my $a = 100;
    my $b = 0;
    my $c = $a / $b;
};
print $@;
print "end\n";
$ perl sample2005.pl
Illegal division by zero at sample2005.pl line 7.
end

通常、エラーが発生するとその時点でプログラムは終了しますが、eval でエラーを捕捉したため、プログラムは最後まで実行されていることがわかります。エラーが発生した場合、その内容を表す文字列が変数 $@ にセットされます。ノーエラーの場合、$@ の値は未定義となります。

eval によるエラーの捕捉は、システム内部で発生するエラーだけではなく、関数 die を使ってユーザーが生成したエラーも捕まえることができます。

リスト : Perl の例外処理 (sample2006.pl)

use strict;
use warnings;

eval {
    die "user error !!\n";
};
print $@;
print "end\n";
$ perl sample2006.pl
user error !!
end

このように、die で発生したエラーも捕まえることができます。この時、$@ には die のメッセージがセットされます。

ここで、Perl の eval は全てのエラーを捕捉することに注意してください。捕捉したエラーの種類は、$@ にセットされたエラーメッセージを調べてみないとわからないのです。たとえば、0で除算した計算は無視して次の計算へ進みたい場合、$@ の文字列が "Illegal division by zero" であることをチェックしないといけません。ほかの言語、たとえば Java, Python, Ruby などでは、捕捉するエラーの種別を指定することができます。

ほかの言語に比べるとあまりスマートではありませんが、eval と die を使えば Perl でも例外処理をプログラムすることができます。

●大域脱出

Perl の例外は、eval のブロックの中で呼び出した関数の中で例外が送出されても、それを捕捉することができます。この機能を使って、評価中の関数からほかの関数へ制御を移す「大域脱出 (global exti)」を実現することができます。

簡単な例を示しましょう。

リスト : 大域脱出 (sample2007.pl)

use strict;
use warnings;

sub bar1 {
    print "call bar1\n";
}

sub bar2 {
    die "Global Exit\n";
}

sub bar3 {
    print "call bar3\n";
}

sub foo {
    bar1(); bar2(); bar3();
}

eval {
    foo();
};
print $@;
$ perl sample2007.pl
call bar1
Global Exit

実行の様子を下図に示します。

通常の関数呼び出しでは、呼び出し元の関数に制御が戻ります。ところが bar2 で die が実行されると、呼び出し元の関数 foo を飛び越えて、制御が eval に移るのです。このように、例外処理を使って関数を飛び越えて制御を移すことができます。

例外処理を使った大域脱出はとても強力な機能ですが、多用すると処理の流れがわからなくなる、いわゆる「スパゲッティプログラム」になってしまいます。使用には十分ご注意下さい。

●パズル Four Fours

最後に、もうひとつパズルを解いてみましょう。Four Fours は数字を使ったパズルです。いろいろなルールがあるのですが、今回は簡易ルールで行きましょう。それでは問題です。

[問題] Four Fours

数字 4 を 4 つと+, -, ×, ÷, (, ) を使って、答えが 1 から 10 になる式を作りなさい。数字は 4 だけではなく、44 や 444 のように合体させてもよい。また、-を符号として使うことは禁止する。

数字の 4 を 4 つ使うので Four Fours という名前なのだと思います。ところで、このルールでは 11 になる式を作ることができません。ほかのルール、たとえば小数点を付け加えると、次のように作ることができます。

4 ÷ .4 + 4 ÷ 4 = 11

今回は簡易ルールということで、小数点を使わないで 1 から 10 までの式を作ってください。まずは、ご自分の頭を使って解いてみましょう。気分転換や息抜きのときにでも考えてみてください。

●数式のパターン

それではプログラムを作りましょう。基本的には、数式を生成して答えをチェックするだけです。Four Four's の場合、4 つの数値に 3 つの演算子しかありませんから、数式のパターンは簡単に求めることができます。数式を二分木で表すと、次に示す 5 つのパターンになります。

X, Y, Z が演算子を表します。これを式で表すと、次のようになります。

  1. (4 Y 4) X (4 Z 4)
  2. 4 X (4 Y (4 Z 4))
  3. ((4 Z 4) Y 4) X 4
  4. 4 X ((4 Z 4) Y 4)
  5. (4 Y (4 Z 4)) X 4

あとは、X, Y, Z に演算子 +, -, *, / を入れて数式を計算すればいいわけです。

Four Four's は数字を合体できるので、数字が 3 つで演算子が 2 つ、数字が 2 つで演算子がひとつ、というパターンもあります。演算子がひとつの場合は簡単ですね。演算子が 2 つの場合は、次の式になります。

  1. (a Y b) X c
  2. a X (b Y c)

a, b, c が数字で X, Y が演算子を表しています。数字は 4 か 44 になります。この場合、a, b, c の組み合わせを生成する必要があります。組み合わせを (a, b, c) で表すと、(4, 4, 44), (4, 44, 4), (44, 4, 4) の 3 通りとなります。これと演算子の組み合わせにより数式を生成して、答えを求めてチェックします。

●プログラムの作成

それでは、4 が 4 つと演算子が 3 つある場合のプログラムを示します。

リスト:数字が 4 つある場合

# チェック
sub check {
    my $expr = shift;
    my $r = eval $expr;
    if ($@) {
        unless ($@ =~ /Illegal division by zero/) {
            die;
        }
    } else {
        if ($r == int($r) && $r >= 1 && $r <= 10) {
            push @{$answer[$r]}, $expr;
        }
    }
}

# 数値が4つの場合
sub solver4 {
    my @operator = ('+', '-', '*', '/');
    foreach my $x (@operator) {
        foreach my $y (@operator) {
            foreach my $z (@operator) {
                foreach my $expr ("(4 $y 4) $x (4 $z 4)",
                                  "4 $x (4 $y (4 $z 4))",
                                  "((4 $z 4) $y 4) $x 4",
                                  "4 $x ((4 $z 4) $y 4)",
                                  "(4 $y (4 $z 4)) $x 4" ) {
                    check($expr);
                }
            }
        }
    }
}

数式の組み立ては、文字列の変数置換を使えば簡単です。演算子を変数 $x, $y, $z にセットし、それを使って 5 種類の数式を組み立てればいいわけです。そして、組み立てた数式 $expr を関数 check に渡して、その中で eval で実行します。

組み立てた数式の中には 0 で除算する場合があります。たとえば 4 * 4 / (4 - 4) を実行すると、実行時にエラー "Illegal division by zero" が発生します。まず、変数 $@ をチェックします。ノーエラーの場合、$@ には未定義値がセットされています。そして、そのエラーが division by zero でなければ、die を呼び出して終了します。この場合、die には $@ が引数として渡されます。

Perl の場合、除算は浮動小数点で実行されることにも注意してください。結果 $r を関数 int で整数に変換しても同じ値であれば、$r が整数値であることがわかります。その値が 1 以上 10 以下であれば、数式を配列 @answer に格納します。@answer の要素は「無名の配列」で初期化しておいて、複数の数式を格納できるようにします。

数字が 3 つで演算子が 2 つの場合も、基本的には同じプログラムになります。ただし、数字は引数として渡します。関数名を solver3 とすると、solver3(4, 4, 44), slover3(4, 44, 4), solver3(44, 4, 4) と 3 回呼び出します。solver3 では、引数と演算子を組み合わせて数式を生成し、答えをチェックします。詳細は プログラムリスト を参照してください。

●実行結果

さっそく実行してみたところ、全部で 100 通りの式が出力されました。このプログラムは重複解のチェックを行っていないので、多数の式が出力されることに注意してください。

実行結果の一部を示します。
 1: (4 - 4) + (4 / 4)
 2: (4 / 4) + (4 / 4)
 3: ((4 + 4) + 4) / 4
 4: 4 + (4 * (4 - 4))
 5: ((4 * 4) + 4) / 4
 6: ((4 + 4) / 4) + 4
 7: 4 + (4 - (4 / 4))
 8: (4 + 4) + (4 - 4)
 9: (4 + 4) + (4 / 4)
10: (44 - 4) / 4

この中で、10 になる式は (44 - 4) / 4 しかありません。数字 4 を 4 つと+, -, ×, ÷, ( , ) だけでは、10 になる式を作ることはできないのですね。


●プログラムリスト

#
# four.pl : 「4つの4」
#
#            Copyright (C) 2015-2023 Makoto Hiroi
#
use strict;
use warnings;

# 答えを格納する配列
our @answer = (
    [],                       # dummy
    [],  [],  [],  [],  [],
    [],  [],  [],  [],  [],
    );

# チェック
sub check {
    my $expr = shift;
    my $r = eval $expr;
    if ($@) {
        unless ($@ =~ /Illegal division by zero/) {
            die;
        }
    } else {
        if ($r == int($r) && $r >= 1 && $r <= 10) {
            push @{$answer[$r]}, $expr;
        }
    }
}

# 数値が4つの場合
sub solver4 {
    my @operator = ('+', '-', '*', '/');
    foreach my $x (@operator) {
        foreach my $y (@operator) {
            foreach my $z (@operator) {
                foreach my $expr ("(4 $y 4) $x (4 $z 4)",
                                  "4 $x (4 $y (4 $z 4))",
                                  "((4 $z 4) $y 4) $x 4",
                                  "4 $x ((4 $z 4) $y 4)",
                                  "(4 $y (4 $z 4)) $x 4" ) {
                    check($expr);
                }
            }
        }
    }
}

# 数値が3つの場合
sub solver3 {
    my ($a, $b, $c) = @_;
    my @operator = ('+', '-', '*', '/');
    foreach my $x (@operator) {
        foreach my $y (@operator) {
            foreach my $expr ("($a $y $b) $x $c",
                              "$a $x ($b $y $c)") {
                check($expr);
            }
        }
    }
}

# 数値が2つの場合
sub solver2 {
    my ($a, $b) = @_;
    my @operator = ('+', '-', '*', '/');
    foreach  my $x (@operator) {
        my $expr = "$a $x $b";
        check($expr);
    }
}

# 解の探索
solver4();
solver3(4, 4, 44);
solver3(4, 44, 4);
solver3(44, 4, 4);
solver2(44, 44);
solver2(444, 4);
solver2(4, 444);

# 答えを出力
foreach my $i (1 .. 10) {
    foreach my $expr (@{$answer[$i]}) {
        print "$i: $expr\n";
    }
}

初版 2015 年 7 月 19 日
改訂 2023 年 3 月 21 日

Copyright (C) 2015-2023 Makoto Hiroi
All rights reserved.

[ PrevPage | Perl | NextPage ]