前回は後方参照、s 演算子による文字列の置換、tr 演算子による文字の置換など、正規表現を含む文字列処理について説明しました。今回は「関数」について説明します。
プログラミングは模型を組み立てる作業と似ています。簡単な処理は Perl の組み込み関数を使って実現することができます。つまり、組み込み関数が部品に相当し、それを使って全体を組み立てるのです。ところが、模型が大きくなると、一度に全部を作るのは難しくなりますね。そのような場合、全体をいくつかに分割して、まずその部分ごとに作ります。最後に、それを結合して全体を完成させます。
これはプログラミングにも当てはまります。実現しようとする処理が複雑になると、一度に全部作ることは難しくなります。そこで、全体を小さな処理に分割して、個々の処理を作成し、それらを組み合わせて全体のプログラムを完成させます [*1]。
分割した処理を作成する場合、それを組み込み関数のようにひとつの部品として扱えると便利です。つまり、小さな部品を作り、それを使って大きな部品を作り、最後にそれを組み合わせて全体を完成させるのです。近代的なプログラミング言語では、ユーザーが部品を作って、それを簡単に使うことができるようになっています。Perl の場合、もっとも基本となる部品が関数です。
Perl の関数定義はとても簡単です。
リスト : Perl の関数定義 sub 関数名 { 処理A; ..... 処理Z; }
関数定義は「sub 名前 ブロック」という構造です。関数はプログラム内のどこで定義してもかまいません。ここで、「ちょっとおかしいな」と気づいた方もいるでしょう。一般的なプログラミング言語とは決定的に異なるところがあるのです。それは、引数 (関数がデータを受け取るための変数) の定義がないことです。Perl の場合、引数は配列 @_ に格納されます。次の例を見てください。
リスト : 関数 foo の定義 use strict; use warnings; sub foo { print "[@_]\n"; } foo; # [] と表示 foo(); # [] と表示 foo(1); # [1] と表示 foo(1,2); # [1 2] と表示
最初に、関数 foo を定義します。すでに同じ名前の変数や配列が定義されていてもかまいません。関数は名前に & をつけて呼び出します。引数はカッコ ( ) 内に指定します。引数がない場合はカッコ ( ) を省略することができます。Perl 5 の場合、カッコがついているか、前もって関数を定義 (または宣言) していれば、& を省略することができます。関数の宣言は「sub 名前;」で行います。もちろん & をつけて呼び出しても問題はありません。
foo は @_ を表示するだけです。この例からもわかるように、Perl の関数は引数の個数に制限はありません。また、関数内で shift を引数なしで使うと、特殊変数 @ARGV ではなく @_ が操作対象となります。ご注意くださいませ。
今の例では、引数はスカラーでしたが、配列の場合はどうなるのでしょうか。実際に試してみましょう。
@a = (10, 20, 30); @b = (40, 50, 60); foo(@a); # [10 20 30] と表示 foo(1, @a); # [1 10 20 30] と表示 foo(@a, @b); # [10 20 30 40 50 60] と表示
リストの場合と同じく、配列は @_ の中に展開されてしまうのです。最後の例のように 2 つの配列を引数として与えると、それらは展開されてしまうので、foo から見ると、2 つの配列を区別することができなくなります。これで困る場合は、「型グロブ」か「リファレンス」 [*2] を使います。これらの方法は、あとで詳しく説明したいと思います。
Perl の場合、関数の返り値はブロックの最後で実行された処理結果となります。また、C言語や Java のように、return を使って値を返すこともできます。値を返す式は、関数が実行されたコンテキストの影響を受けます。次の例を見てください。
リスト : 関数の返り値 use strict; use warnings; sub foo1 { return (10, 20, 30); } sub foo2 { return 10; } my @a = foo1; my $b = foo1; print "[@a] $b\n"; # [10 20 30] 30 と表示する @a = foo2; $b = foo2; print "[@a] $b\n"; # [10] 10 と表示する
関数 foo1 はリスト (10, 20, 30) を返します。Perl では、配列を返す場合でも、リストに展開されてから返されます。変数 @a に代入する場合、リストコンテキストなので、リストがそのまま配列 @a に代入されます。
変数 $b に代入する場合は、スカラーコンテキストで評価されます。Perl では、リストをスカラーコンテキストで評価すると、リストの最後の要素が取り出されます。したがって、変数 $b には 30 が代入されるのです。配列 @a をスカラーコンテキストで評価すると要素数を返しますが、リストの評価結果は違うので注意してください。
関数 foo2 は 10 を返します。@a への代入はリストコンテキストなので、リスト (10) に変換されてから代入されます。スカラーコンテキストの場合は、そのまま 10 が変数 $b に代入されます。こちらは簡単ですね。
関数を使うのであれば、変数の話を避けて通るわけにはいきません。次の例を見てください。
リスト : 局所変数の動作 (sample0501.pl) use strict; use warnings; sub foo1 { print "foo1 [@_]\n"; foo2(3, 4, 5); print "foo1 [@_]\n"; } sub foo2 { print "foo2 [@_]\n"; foo3(6, 7, 8, 9); print "foo2 [@_]\n"; } sub foo3 { print "foo3 [@_]\n"; } foo1(1, 2);
$ perl sample0501.pl foo1 [1 2] # (1) foo2 [3 4 5] # (2) foo3 [6 7 8 9] # (3) foo2 [3 4 5] # (4) foo1 [1 2] # (5)
関数 foo1 から foo2 を呼び出し、foo2 から foo3 を呼び出しています。このとき、配列 @_ の内容を表示しています。実行結果を見ると、関数を呼び出すたびに @_ に引数が格納されていくのがわかりますね。
では、関数呼び出しが終了すると @_ はどうなるのでしょうか。foo3 の実行が終了して foo2 に戻る (4) を注目してください。@_ の値は foo2 が呼び出された (2) と同じですね。foo3 では (6, 7, 8, 9) だったのに、foo2 に戻ると元の値に戻るのです。foo2 から foo1 に戻る (5) の場合も同じです。
このように、@_ の値は関数が実行されているあいだだけ有効なのです。このような変数を「局所変数 (local variable)」といい、それ以外の変数を「大域変数 (global variable)」といいます。大域変数は Perl が実行されている間であれば、どこからでも値を読み書きすることができます。
以前の Perl では、変数を宣言せずに使用することができました。これが「大域変数」になります。ところが、use strict; を指定すると、変数の宣言が必要になるため、この方法で大域変数を定義することはできません。この場合は our で変数を宣言します。
our 変数名; out 変数名 = 初期値;
our で宣言された変数は大域変数になります。また、my で変数を宣言する場合でも、関数の外側で宣言すると、同じファイル内であれば大域変数のように扱うことができます。これはあとで説明します。
最初に説明しましたが、プログラムを作る場合、関数を部品のように使います。ある関数を呼び出したら、いままで使っていた変数の値が書き換えられていた、というのでは呼び出す方が困ってしまいますね。部品であるならば、ほかの処理に影響を及ぼさないように、自分自身の中で処理を完結させることが望ましいのです。これを実現するための必須機能が局所変数なのです。
局所変数を定義するには my または local を使います。ここでは my を使って説明しましょう。次の例を見てください。
リスト : 局所変数の使用例 (sample0502.pl) use strict; use warnings; our $x = 100; # my $x = 100; でも OK our $y = 200; # my $y = 200; でも OK sub foo { my $x; $x = 10; $y = 20; print "x = $x, y = $y\n"; } print "x = $x, y = $y\n"; foo; print "x = $x, y = $y\n";
$ perl sample0502.pl x = 100, y = 200 x = 10, y = 20 x = 100, y = 20
ファイルの先頭で定義されている変数 $x, $y が大域変数になり、関数 foo の中で宣言した変数 $x が局所変数になります。局所変数が値を保持する期間のことを、変数の「有効範囲 (scope : スコープ)」といいます。Perl の場合、変数を定義したブロック内が有効範囲と考えてください。
関数の定義はブロック { } で囲まれていますね。@_ はこのブロックの中で暗黙のうちに定義された局所変数と考えることができます。したがって、@_ の値はこのブロックの中でのみ有効です。関数の実行が終了する、つまり、ブロックから抜けた時点で、@_ の値は破棄されるのです。my で宣言された変数も、定義したブロック内が有効範囲となります。
$x と $y の値を表示すると 100, 200 となります。次に foo を呼び出します。foo の中では $x と $y に値を代入していますね。その結果は 10, 20 となります。次に、foo の実行が終了すると、$x と $y の値は 100, 20 になりました。$y の値が書き換えられていることに注目してください。
foo の中では $x は局所変数なので、値を代入してもその影響は関数が実行されているあいだだけに限られます。ところが、$y は局所変数として宣言されていない、つまり大域変数なので、値を書き換えてしまうと、関数の実行が終了してもその影響が残ります。したがって、foo の実行が終了すると、$x の値は元のままですが、$y の値は foo で代入された値 20 となるのです。
ところで、our で宣言した変数 $x, $y は、my で宣言しても正常に動作します。ファイルは全体がブロックで囲まれていると考えてください。したがって、ファイルの先頭で my で宣言された変数は、ファイル全体で有効になります。同じファイル内であればどこからでもアクセスすることができ、プログラムが終了するまで有効です。つまり、大域変数のように扱うことができます。
もう一つ簡単な例を示しましょう。
リスト : 局所変数の使用例2 (sample0503.pl) use strict; use warnings; sub foo { my $x = 10; print "$x\n"; { my $x = 20; print "$x\n"; } print "$x\n"; } foo;
$ perl sample0503.pl 10 20 10
関数 foo の中のブロックで $x を定義していますね。この変数はこのブロックでのみ有効なのです。したがって、ブロックを抜けたら $x の値は元の値に戻ります。
ところで、もうひとつの local ですが、基本的な使い方は my と同じです。ですが、local と my ではスコープ規則 [*3] に違いがあり、Perl 5 では my を使うことを強くお勧めします。いまこの違いを説明すると、初心者の方は混乱するだけかもしれません。ここでは、local よりも my を使った方が変数の有効範囲が明確になる、と考えてください。
それからもうひとつ、配列 @_ に値を代入するときは注意が必要です。値を参照するだけならば、直接 @_ にアクセスしてもいいのですが、それ以外の場合は、局所変数に代入してから使うことを強くお勧めします。
理由を簡単に説明すると、各引数は @_ に代入されるのではなく、「参照渡し」されるのです。もし @_ に値を代入すると、対応する引数の値も書き換えてしまいます。参照渡しの概念はリファレンスに通じるものなので、ここでは詳しく説明しません。釈然としないでしょうが、お約束として覚えてください。いまは「習うより慣れろ」ということで、関数を使ってプログラムをどんどん作りましょう。
それでは簡単な例題として、4 つの数字を当てる「マスターマインド」というゲームを作ってみましょう。まず、0 から 9 までの中から重複しないように、数字を 4 つ選びます。私たちは数字だけではなく、その位置も当てなくてはいけません。数字は合っているが位置が間違っている個数を cows で表し、数字も位置も合っている個数を bulls で表します。つまり、bulls が 4 になると正解というわけです。
言葉で説明するとわかりにくいので、ゲームの進行状況を見てみましょう。
6 2 8 1 --------------------------------- 1. 0 1 2 3 : cows 2 : bulls 0 2. 1 0 4 5 : cows 1 : bulls 0 3. 2 3 5 6 : cows 2 : bulls 0 4. 3 2 7 4 : cows 0 : bulls 1 5. 3 6 0 8 : cows 2 : bulls 0 6. 6 2 8 1 : cows 0 : bulls 4 おめでとう!! 図 : マスターマインドの動作例
4 つの数字は配列に格納することにします。正解は 6 2 8 1 です。プレーヤーは、最初に 0 1 2 3 を入力しました。0 と 3 は 6 2 8 1 に含まれていませんね。1 と 2 は 6 2 8 1 の中にあるのですが、位置が異なっているので cows が 2 となります。この場合、bulls は 0 です。あとは bulls が 4 になるように数字を選んで入力していきます。4 番目の入力では、2 の位置が合っているので bulls は 1 となります。この例では 6 回で正解となりました。
それでは、順番に処理内容を考えていきましょう。まず、数字を 4 つ決めないことには、ゲームを始めることができませんね。コンピュータで適当な数字を選ぶためには「乱数 (random numbers)」という方法を使います。
私たちが適当な数字を決める場合、サイコロを使えば 1 から 6 までの数字を簡単に決めることができます。たとえば、サイコロを振って出た目を記録したら、次のようになったとしましょう。
5, 2, 1, 2, 6, 3, 4, 3, 1, 5, .....
サイコロの目は、イカサマをしないかぎり、出る確率が 1/6 で規則性はまったくありません。したがって、数字の出る順番には規則性はなく、まったくでたらめになります。いま 2 が出たから次は 1 が出るとか、3, 4 と続いたから次は 5 が出る、などのように、前に出た数字から次に出る数字を予測することはできないのです。このように、でたらめに並んだ数列を「乱数列」といい、乱数列の中のひとつひとつの数字を「乱数」といいます。
コンピュータは、決められた手順 (プログラム) を高速に実行することは得意なのですが、まったくでたらめの数を作れといわれると、とたんに困ってしまいます。そこで、何かしらの数式をプログラムして、それを実行することで乱数を発生させます。厳密にいえば乱数ではありませんが、それを乱数としてみなして使うことにするのです。このような乱数を「疑似乱数」といいます。現在では、ほとんどのコンピュータが、この疑似乱数を使っています。
Perl の場合、乱数を発生させるには関数 rand を使います。
rand 式 rand
rand は 0 から式を評価した値まで範囲の乱数 (浮動小数点数) を返します。式の評価結果は正の整数値でなければいけません。式を省略した場合は、0 から 1 まで (1 は含まない) の浮動小数点数を返します。浮動小数点数を整数値に変換する場合は関数 int を使います。0 から 9 までの乱数を発生させるには、次のようにします。
my $r = int(rand(9));
それでは乱数を 5 個だけ作ってみましょう。次のプログラムを実行してください。
リスト : 乱数の発生 (sample0504.pl) use strict; use warnings; foreach (1 .. 5) { print int(rand(9)), "\n"; }
この実行結果は次のようになります。
$ perl sample0504.pl 7 6 8 2 7 $ perl sample0504.pl 7 3 1 3 1 $ perl sample0504.pl 4 0 1 0 1
プログラムを実行するたびに、異なる乱数列が生成されます。
それでは、数字を 4 つ決める処理を作りましょう。簡単に考えると、処理手順を下図にのようになります。
├←───────┐ ↓ │ ┌─────────┐ │ │0 - 9 の数字を発生│ │ └─────────┘ │ ↓ │ ┌─────────┐ │ │同じ数字があるか?│──→┤ └─────────┘Yes │ ↓No │ ┌─────────┐ │ │ 数字を配列に追加 │ │ └─────────┘ │ ↓ │ ┌─────────┐ │ │ 4つ揃ったか? │──→┘ └─────────┘No ↓Yes 図 : 数字を決める流れ図
0 - 9 の数字を発生させる処理は、乱数を使えば簡単に実現できますね。今回は、この数字を配列 @collect に格納します。この変数は大域変数として定義します。数字を配列に追加する前に、同じ数字があるかチェックします。もし、同じ数字があれば、配列に追加しないで、数字を発生させる処理に戻ります。違う数字であれば配列に追加します。あとは数字が 4 つそろうまで、この処理を繰り返します。
最初に、@collect の中に同じ数字があるかチェックする関数 find を作りましょう。
リスト : 同じ数字が @collect にあるか sub find { my $n = shift; foreach my $x (@collect) { return 1 if ($x == $n); } 0; }
まず、shift で引数を取り出し、変数 $n にセットします。これと同じ値が配列 @collect にあるかチェックします。これは foreach を使って、配列の内容をひとつずつ取り出して比較すればいいですね。同じ数字があれば 1 を、なければ 0 を返します。
次は、数字を 4 つ決める関数 make_collect を作ります。
リスト : 数字を 4 つ決める sub make_collect { @collect = (); while (@collect < 4) { my $n = int(rand(9)); unless (find($n)) { push @collect, $n; } } }
最初に配列 @collect を空に初期化します。@collect はファイルの先頭で定義します。次に、この配列の中に数字が 4 つセットされるまで繰り返します。@collect はスカラーコンテキストで評価されると、格納されている要素数を返すことを思い出してください。
乱数で選んだ数字は局所変数 $n にセットします。次に、関数 find を呼び出して、$n と同じ数字が @collect にないことを確認します。find は同じ数字があると真 (1) を返しますが、unless を使っているので偽 (0) を返したときに条件が成立することになります。配列のセットには push を使います。C言語と違い、Perl では添字を使わなくても配列にアクセスすることができます。
次は、数字を入力する処理を作ります。数字は標準入力 STDIN から 4 つまとめて入力し、数字は空白で区切ってもらうことにします。このような仕様にすると、簡単にプログラムを作ることができます。関数名は input_numbers としました。
リスト : 異なる数字を4つ STDIN から入力する sub input_numbers { while (<>) { chop; my @input = split(/ /, $_); if (@input != 4) { print "4個の異なった数字を入力してください\n"; } elsif (check_number(@input)) { return @input; } } }
入力は行入力演算子 <> を使えばいいですね。chop で改行を取り除き、split で文字列を空白で切り分けて、配列 @input にセットします。@input の要素数が 4 でなければエラーメッセージを表示し、数字を再入力してもらいます。関数 check_number は、@input に重複した数字がないかチェックします。4 つの数字がすべて異なっていれば真 (1) を返すので、return で @input を返します。
次は、関数 check_number を作ります。
リスト : 重複した数字がないか sub check_number { my @table = (0, 0, 0, 0, 0, 0, 0, 0, 0, 0); foreach my $n (@_) { if ($n < 0 || $n > 9) { print "0 - 9 の数字を入力してください\n"; return 0; } elsif ($table[$n]) { print "数字が重複しています\n"; return 0; } else { $table[$n] = 1; } } 1; }
配列 @table は入力された数字のチェックに使います。@table を 0 に初期化し、入力された数字には 1 をセットします。もしも、@table に 1 がセットされていれば、同じ数字が入力されたことがわかります。
次の foreach で、引数をひとつずつ取り出して範囲チェックと重複チェックを行います。異常が見つかったならば、エラーメッセージを出力して return で 0 を返します。それ以外の場合、数字は正常なので @table に 1 をセットします。数字がすべて正常であれば 1 を返します。
それでは、数当てゲームの処理内容を考えていきましょう。このゲームは、入力された答と正解から bulls の個数をカウントし、それが 4 になればゲーム終了、そうでなければ入力処理に戻ります。この処理を図に示すと、次のようになるでしょう。
↓ ┌──────┐ │数字を決める│ └──────┘ ├←────────┐ ↓ │ ┌──────┐ │ │ 答の入力 │ │ └──────┘ │ ↓ │ ┌───────┐ │ │bulls を数える│ │ └───────┘ │ ↓ │ ┌───┐ ┌────────┐ │ │正解!│←──│bulls は4個か?│ │ └───┘ Yes └────────┘ │ ↓No │ ┌───────────┐ │ │bulls とcowsを表示する│ │ └───────────┘ │ │ │ └─────────┘ 図 : 数当てゲームの流れ図
この処理の中で、bulls と cows を数える処理を新しく作る必要があります。まず、bulls を求める関数 count_bulls を作りましょう。
リスト : bulls を数える sub count_bulls { my ($bulls, $i) = (0, 0); for (; $i < 4; $i++) { $bulls++ if $collect[$i] == $_[$i]; } $bulls; }
これは簡単ですね。引数 @_ と @collect を順番に比較していき、同じ数字であれば $bulls を +1 するだけです。局所変数 $i は my で 0 に初期化しているので、for 文の初期化式は空文となっています。for 文では、条件式や更新式も空文にすることができます。すべてを空文にする、つまり for (;;) { ... } とすると、無限ループとなります。これはC言語の for 文でも同じです。
次は cows を数える処理です。いきなり cows を数えるのは難しいですが、2 つの配列に共通の数字を数えることは簡単にできます。この方法では、bulls の個数もいっしょに数えることになるので、そこから bulls を引くことで cows を求めます。プログラムは次のようになります。
リスト : 同じ数字を数える sub count_same_number { my $c = 0; foreach my $n (@_) { $c++ if find($n); } $c; }
関数 count_same_number は、@collect と入力データ @input の共通の数字の個数を求めます。foreach で引数 @_ から数字をひとつ取り出して、それが配列 @collect にあるかチェックします。これは数字を 4 つ決めるときに作った関数 find を呼び出せばいいですね。このように部品として関数を作っておけば、それを再利用することができます。
必要な関数が揃ったので、ゲーム本体を作りましょう。
リスト : ゲームの実行 make_collect; my $count = 0; my $bulls = 0; while ($bulls != 4) { my @ans = input_numbers; $count++; $bulls = count_bulls(@ans); my $cows = count_same_number(@ans) - $bulls; print "回数 $count : @ans : cows $cows : bulls $bulls\n"; } print "おめでとう!!\n";
最初に make_collect で正解となるコードを作ります。変数 $count は試行回数をカウントします。変数 $bulls は bulls の個数を、$cows は cows の個数を表します。input_number を呼び出して、入力されたコードを配列 @ans にセットします。
あとは、count_bulls と count_same_number で bulls の cows の個数を求め、それを print で表示します。$bulls が 4 ならば正解です。while 文を終了して、print で おめでとう!! を表示します。
プログラムはこれで完成です。それでは実行してみましょう。
$ perl mastermind.pl 0 1 2 3 回数 1 : 0 1 2 3 : cows 2 : bulls 0 4 5 6 7 回数 2 : 4 5 6 7 : cows 2 : bulls 0 6 7 0 1 回数 3 : 6 7 0 1 : cows 2 : bulls 0 2 0 4 6 回数 4 : 2 0 4 6 : cows 0 : bulls 1 3 0 7 5 回数 5 : 3 0 7 5 : cows 2 : bulls 0 1 3 5 6 回数 6 : 1 3 5 6 : cows 2 : bulls 2 5 3 1 6 回数 7 : 5 3 1 6 : cows 0 : bulls 4 おめでとう!!
7 回で当てることができました。入力をうながすプロンプトを表示した方が良かったですね。簡単に改造できるので、試してみてください。
今回はここまでです。次回は「再帰定義」を中心に関数の使い方を説明します。
# # mastermind.pl : マスターマインド # # Copyright (C) 2015-2023 Makoto Hiroi # use strict; use warnings; my @collect; # 同じ数字が @collect にあるか? sub find { my $n = shift; foreach my $x (@collect) { return 1 if ($x == $n); } 0; } # 4 つの数字を決める sub make_collect { @collect = (); while (@collect < 4) { my $n = int(rand(9)); unless (find($n)) { push @collect, $n; } } } # 重複した数字がないか sub check_number { my @table = (0, 0, 0, 0, 0, 0, 0, 0, 0, 0); foreach my $n (@_) { if ($n < 0 || $n > 9) { print "0 - 9 の数字を入力してください\n"; return 0; } elsif ($table[$n]) { print "数字が重複しています\n"; return 0; } else { $table[$n] = 1; } } 1; } # 数字の入力 sub input_numbers { while (<>) { chop; my @input = split(/ /, $_ ); if (@input != 4) { print "4個の異なった数字を入力してください\n"; } elsif (check_number(@input)) { return @input; } } } # bulls を数える sub count_bulls { my ($bulls, $i) = (0, 0); for (; $i < 4; $i++) { $bulls++ if $collect[$i] == $_[$i]; } $bulls; } # 同じ数字を数える sub count_same_number { my $c = 0; foreach my $n (@_) { $c++ if find($n); } $c; } # ゲームの実行 make_collect; my $count = 0; my $bulls = 0; while ($bulls != 4) { my @ans = input_numbers; $count++; $bulls = count_bulls(@ans); my $cows = count_same_number(@ans) - $bulls; print "回数 $count : @ans : cows $cows : bulls $bulls\n"; } print "おめでとう!!\n";