M.Hiroi's Home Page

Linux Programming

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

[ PrevPage | Perl | NextPage ]

リファレンス (後編)

前回はC言語のポインタと比較しながら、リファレンスの基本的な使い方を説明しました。今回も引き続きリファレンスの使い方を説明します。

●無名のハッシュ

Perl 5 では中カッコ { } を使うことで、新しい連想配列 (ハッシュ) を作成することができます。角カッコで生成する「無名の配列」と同様に、これを「無名のハッシュ」と呼びます。簡単な使用例を示しましょう。

my $rh1 = {};    # 空のハッシュを生成し、そのリファレンスをセット
my $rh2 = {abc => 10, def => 20};  # 要素を含んだ無名のハッシュ

my %h = (abc => 10, def => 20, ghi => 30);
my $rh3 = {%h};  # ハッシュ %h の内容で初期化

{ } の中には、文字列や定数だけではなく、変数や式を使うことができます。また、「キー => 値」の値には、無名の配列や無名のハッシュを組み合わせることもできます。

名無しのハッシュといって甘く見てはいけないのは、無名の配列と同じです。無名のハッシュを使えば、複雑なデータでもわかりやすくプログラミングすることができるのです。C言語には構造体 [*1] というデータ構造がありますが、Perl ではハッシュを使って構造体をシミュレートすることができます。

-- note --------
[*1] 近代的なプログラミング言語には、ユーザーが既存のデータ型を組み合わせて新しいデータを定義する機能があります。C言語では構造体、C++ や Java ではクラスを使って、新しいデータ型を作ることができます。

●関数へのリファレンス

今までのリファレンスと同様に、関数へのリファレンスも簡単に扱うことができます。関数名の前に \ をつけることで、関数へのリファレンスを生成することができます。簡単な使用例を示しましょう。

sub hello {
    print "hello, world\n";
}

my $rs = \&hello;

これで変数 $rs には関数 hello へのリファレンスがセットされます。ここでは、関数へのリファレンスを生成して変数にセットするだけなので、関数 hello は実行されていないことに注意してください。\&hello(); とすると、関数 hello が実行され、その返り値のリファレンスを生成することになります。関数へのリファレンスを生成するときは、関数名の後ろのカッコはつけないように注意してください。

デリファレンスも簡単です。リファレンスを格納している変数の前に & をつけるか、矢印演算子 -> を使います。

&$rs();         # hello, world と表示する
$rs->();        # 同上

●無名の関数

sub の後ろの関数名を省略すると、名前の無い関数を作ることができます。これを「無名の関数」といいます。sub は作成した関数へのリファレンスを返すので、このリファレンスを使って関数を呼び出しします。簡単な使用例を示しましょう。

my $rs1 = sub { print "hello, world\n"; };
&$rs1();   # hello, world と表示する
$rs1->();  # 同上

変数 $rs1 には、sub が定義した関数へのリファレンスがセットされます。通常の関数定義の場合、セミコロン ( ; ) は不要ですが、この場合は変数への代入文なので、セミコロンが必要になります。呼び出し方法は、関数へのリファレンスと同じです。

無名の関数はC言語にはなく、Lisp / Scheme など関数型言語の機能です。Perl 5 でリファレンスをサポートするとき、Lisp から便利な機能を取り込んだのでしょう。実はもうひとつ、Lisp から取り込んだ機能に「クロージャ (closure)」があります。無名関数の本当の力は、クロージャを使うときにこそ発揮されます。クロージャは回を改めて詳しく説明します。それにしても Perl で Lisp ライクな機能が使えるとは、Lisp 好きな M.Hiroi もたいへん驚きました。

●高階関数

関数のリファレンスや無名の関数を使うと、関数を変数に代入したり、引数として関数に渡すことができます。関数型言語の世界では、関数を引数として受け取る関数を「高階関数 (higher order function)」と呼びます。C言語など手続き型言語ユーザーにとって、高階関数は特別な機能と思われるかもしれませんが、関数型言語ではよく使われる機能で、便利な高階関数が多数用意されています。

C言語では「関数へのポインタ」を使って高階関数を実現することができます。C言語の標準ライブラリ関数では、sort や bsearch などが高階関数です。C++ では、標準ライブラリの STL (Standard Template Library) [*2] に、たくさんの高階関数が用意されています。

Perl の組み込み関数では、grep, map, sort が高階関数といえます。ただし、これらの関数は「関数へのリファレンス」を受け取るのではなく、演算子や式、もしくは関数の名前を受け取り、それを実行します。ここで grep, map, sort を簡単に説明しておきましょう。

-- note --------
[*2] <algorithm> の中には Common Lisp でお馴染みの find_if, count_if, remove_if などの関数があります。

●grep

grep というと、正規表現を使ってテキストファイルから文字列を検索するコマンドが有名ですが、Perl の grep はテキストファイルではなく、配列から文字列を検索します。

grep 処理, 配列

grep は配列の各要素を順に、一時的に特殊変数 $_ にセットして、第 1 引数の処理を実行します。そして、その結果が真となった要素を配列に格納して返します。簡単な使用例を示しましょう。

my @a = ('abc', 'ABC', 'def', 'DEF', 'ghi');

my @b = grep(/[A-Z]+/, @a);   # @b => ('ABC', 'DEF')
my $c = grep(/[A-Z]+/, @a);   # $c => 2

この例では、配列 @a から英大文字を含む文字列を取り出して、配列 @b にセットします。grep をスカラーコンテキストで評価すると、結果が真となった要素の個数を返します。したがって $c は 2 となります。

名前を見ると、grep は文字列を検索する関数のようですが、m 演算子ではなく式を与えることで、様々な処理を行うことができます。たとえば、配列の中から奇数を取り出すことは、grep を使えば簡単に行えます。

@a = (1, 2, 3, 4, 5, 6, 7);
@b = grep(($_ & 0x01), @a);    # @b => (1, 3, 5, 7)

配列の要素は $_ に格納されます。$_ と 0x01 の AND が真であれば、その数字は奇数ですね。式 ($_ & 0x01) を grep に渡せば、配列 @a から奇数を取り出すことができるのです。

grep を使う場合、$_ の値を書き換えるときには注意が必要です。$_ は配列の要素を「参照」しているので、$_ の値を書き換えると、配列の要素も同じ値に書き換えられます。つまり、要素はリファレンスで渡され、そのデリファレンスに特殊変数 $_ を使っている、と考えることができるのです。このような引数の渡し方を「参照渡し」といい、関数を「参照呼び出し」する、ともいいます。これと対になるのが「値渡し」とか「値呼び出し」といわれる方法です。この違いについてはあとで詳しく説明します。

それでは簡単な例を示しましょう。

@a = (1, 2, 3, 4, 5, 6, 7);
@b = grep((++$_), @a);        # @a と @b ともに (2, 3, 4, 5, 6, 7, 8)

これは配列の各要素を加算する処理です。式は ++$_ なので、配列の要素の値も +1 され、そのあとで真偽が判断されます。つまり、配列 @a の値が +1 され、その結果が取り出されるため、@a と @b の内容が同じになるのです。式に $_++ が与えられると、$_ の値により真偽が判断され、そのあとで要素が +1 されます。その結果、@a の各要素は +1 されますが、@b には元の値が格納されます。

●map

実は、このような処理に grep を使うのは間違いです。なぜならば、加算した値が 0 の場合、条件不成立と判断されるため、その要素は配列に格納されません。また、元の配列の要素を書き換えると困る場合もあるでしょう。配列の要素に処理を適用して、その評価結果を配列に格納する場合は、関数 map [*3] を使います。

map 処理, 配列

map は grep と同じく、配列の各要素を順に、一時的に特殊変数 $_ にセットして、第 1 引数の処理を実行します。そして、その結果を配列に格納して返します。grep と違い、真偽を判定して要素を格納するのではなく、処理の評価結果を格納するのです。配列の各要素を +1 する処理は、次のようになります。

@b = map(($_ + 1), @a);     # @b => (2, 3, 4, 5, 6, 7, 8)

$_ の値を書き換えていないので、配列 @a の値は変更されません。式の評価結果は配列でもかまいません。すべてひとつの配列に展開されます。

それから、grep, map とも処理にブロックを指定することができます。このとき、ブロックの後ろにカンマ ( , ) をつけてはいけません。無名のハッシュと間違えられます。簡単な例を示しましょう。

@a = (1, 2, 3, 4, 5, 6, 7);
@b = map({ ($_ & 0x01) ? $_ * 2 : $_ } @a);

# @b => (2, 2, 6, 4, 10, 6, 14)
# map {($_ & 0x01) ? $_ * 2 : $_} @a; と書いてもよい

偶数はそのままで奇数を 2 倍にします。ブロックですから、もっと複雑な処理でも書くことができます。

-- note --------
[*3] map も Lisp から取り込んだ機能でしょう。Lisp には複数の map 関数が用意されています。

●sort

最後に sort を説明しましょう。ソート (sort) とは、ある規則に従ってデータを順番に並べることです。たとえば、データが数値であれば、小さい順かもしくは大きい順に並べることになります。

sort 比較処理 配列

sort は第 1 引数の比較処理を省略することができます。その場合は、配列の要素を文字列として比較し、アルファベット順にソートします。簡単な使用例は 正規表現 (前編) で説明しました。それ以外の順番でソートしたい場合は、比較関数を定義して、その名前を渡します。このとき、関数名の後ろにカンマ ( , ) をつけてはいけません。

比較関数には配列の要素が 2 つ渡されますが、通常の関数と違って、@_ ではなく変数 $a と $b に渡されることに注意してください。また、$a と $b には「参照渡し」されるので、比較関数内で $a と $b を書き換えてはいけません。結果は -1, 0, +1 の値を返すようにします。これは演算子 <=> と cmp と同じです。なかなかめんどうな仕様ですが、実行速度を少しでも速くするための工夫なのです。実用を重視する Perl ならではの仕様といえるでしょう。

たとえば、配列を数値としてソートする場合は、次のように行います。

# 比較関数定義
sub numcmp {$a <=> $b;}

@a = (1, 10, 100, 2, 20, 200);
@b = sort numcmp @a;            # sort( numcmp @a ); でもよい

関数 numcmp の $a と $b を逆にすると、逆順にソートすることができます。また、比較関数名のところでブロックを使ってもかまいません。

@b = sort {$b <=> $a;} @a;    # 逆順にソートする

これで、配列 @a を逆順にソートした結果が配列 @b にセットされます。英大文字小文字を区別せずにソートする場合は、文字列を小文字または大文字に変換してから比較します。

@a = ('abc', 'ABC', 'def', 'DEF');

@b = sort {lc($a) cmp lc($b)} @a;    # @b => abc ABC def DEF
@b = sort {"\L$a" cmp "\L$b"} @a;    # 上と同じ

関数 lc は文字列の中の英大文字を小文字に変換して返します。これは " で囲まれた文字列の先頭に \L をつけた場合と同じ働きをします。

●多段階のリファレンス

リファレンスはスカラー型データとして扱われます。ということは、リファレンスを指すリファレンスも作ることができるのです。C言語の場合も、ポインタを指すポインタを作ることができます。次の図を見てください。

変数 q はポインタです。q はポインタ p を指しています。p は変数 i を指しています。つまり、q は p を経由して変数 i を指し示しているのです。これをC言語のプログラムで表すと、次のようになります。

リスト : 多段階のポインタ

int i;
int *p;
int **q;
i = 0x100000;
p = &i;
q = &p;

C言語の場合、ポインタを指し示すポインタは、経由するポインタと同じ数だけアスタリスク ( * ) を追加します。変数 q はポインタ p を経由して変数 i を指し示すので int **q; となります。2 つのポインタを経由するのであれば、int ***q; と宣言します。

ポインタ q はポインタ p を指し示すので、初期化は変数 p のアドレスをセットします。p の値は変数 i のアドレスなので、q にセットしてはいけません。もし q = p; とプログラムすると、コンパイル時にワーニングが表示されます。

これで、**q とすることで変数 i の値にアクセスすることができます。**q =0x999; のように値を代入すると、変数 i の値を書き換えることができます。また、*q とすることで変数 p の値にアクセスすることができます。このとき、*q の値を書き換えると、ポインタ q と p は変数 i ではなく、別の値を指し示すことになります。このように、ポインタを操作するときは細心の注意を払わなければいけないのです。

Perl のリファレンスでも同じことができます。次の例を見てください。

my $i = 0x100000;
my $p = \$i;
my $q = \$p;

print $$$q;     # 1048576 と表示

変数 $p は変数 $i へのリファレンスを格納し、変数 $q は変数 $p へのリファレンスを格納します。したがって、変数 $q は変数 $p を経由して変数 $i を指し示しています。変数 $q の指し示すデータを表示する場合は、経由するリファレンスの数だけ $ を追加します。つまり、$$$q で変数 $i にアクセスすることができます。$$q では変数 $p にアクセスすることになり、この値を書き換えると、変数 $q は変数 $i ではなく別のデータを指し示すことになります。これはC言語のポインタと同じです。

また、Perl では次のような使い方もできます。

my $i = 0x100000;
my $q = \\$i;

print $$$q;     # 1048576 と表示
print $$q;      # たとえば SCALAR(0x280f80) と表示
print $q;       # たとえば SCALAR(0x27c358) と表示

この場合、変数 $i を指すリファレンスと、それを指し示すリファレンスが作成されます。C言語の & 演算子は変数のアドレスを求めるだけなので、このような使い方はできません。

●多次元配列と矢印演算子

リファレンスを使った例題として、多次元配列を実現してみましょう。これはいままで説明した「配列の配列」を使えば簡単です。たとえば、行列 (2 次元配列) ならば次のように定義することができます。

my @m = (
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9]
);

リストを使って配列 @m にデータをセットします。このとき、リストの中で無名の配列を使います。すると、配列 @m の各要素は、無名の配列へのリファレンスとなります。各要素へのアクセスは次のように行います。

print $m[0]->[0];      # (0,0) にある 1 を出力
print $m[2]->[2];      # (2,2) にある 9 を出力

Perl の場合、複数の添字が続く場合に限り、それらの間にある矢印を省略することができます。上の例は次のように書くことができます。

print $m[0][0];
print $m[2][2];

この書き方は、C言語での多次元配列の表記法とよく似ています。C言語ユーザーには馴染みやすいでしょう。また、矢印の省略は配列にかぎらずハッシュでも可能です。

もうひとつ方法があります。それは角カッコの中で角カッコを使う方法です。

my $m = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9]
];

変数 $m は無名の配列へのリファレンスを格納します。角カッコの中で角カッコを使っているので、各要素は無名の配列へのリファレンスとなります。この場合、要素のアクセスは次のようになります。

print $m->[0]->[0];      # (0,0) にある 1 を出力
print $m->[2]->[2];      # (2,2) にある 9 を出力

矢印を省略すると、次のようになります。

print $m->[0][0];
print $m->[2][2];

$m はリファレンスを格納している変数なので、$m と [ ] の間の矢印は省略できません。ご注意くださいませ。

●ファイルからデータを読み込む

今度はファイルからデータを読み込んで配列を作成する方法を説明しましょう。ファイルには次のようにデータが格納されているとします。

1 2 3
4 5 6
7 8 9

3 × 3 の行列であれば、1 行に 3 つの要素を書き込みます。要素と要素の間は空白で区切ります。このようなデータは、次のように読み込むことができます。

リスト : 配列データの読み込み (1)

my @m;
while(<>){
    push @m, [split];
}

読み込んだデータは配列 @m にセットします。行入力演算子で標準入力から 1 行読み込みます。データは特殊変数 $_ にセットされます。それを split で分割します。split はリストコンテキストで評価されると、分割した要素をリストに格納して返します。このリストが無名配列の初期値となり、そのリファレンスが push により配列 @m にセットされます。

変数 $m に配列へのリファレンスをセットしたい場合は、次のように行います。

リスト : 配列データの読み込み (2)

my $m = [];
while(<>){
    push @$m, [split];
}

まず $m に無名の配列をセットします。次に、ファイルからデータを読み込んでsplit で分割し、push で配列にセットします。push は引数に配列を必要とするので、$m を渡すのではなく @$m とデリファレンスしています。

ちなみに、変数 $m を無名の配列に初期化する処理は必要ありません。最初にpush が実行されるとき、Perl が自動的に無名の配列を作成してくれるからです。

●二次元配列の表示

今度は、読み込んだデータを表示してみましょう。これも簡単にプログラムできます。

リスト : 配列データの表示 (1)

foreach my $a (@m) {
    print "@$a\n"
}

配列 @m に格納されているデータを表示します。foreach で要素を取り出して変数 $a にセットします。$a には配列へのリファレンスがセットされるので、@$a とデリファレンスしてやれば、" での変数展開により配列の内容を出力できます。

また、for 文を使って各要素にアクセスすると、次のようになります。

リスト : 配列データの表示 (2)

for (my $i = 0; $i < @m; $i++) {
    for (my $j = 0; $j < @{$m[$i]}; $j++) {
        print "$m[$i][$j] ";
    }
    print "\n";
}

配列をスカラーコンテキストで評価すると要素数を返します。2 番目の for 文がちょっと複雑かもしれません。これが嫌であれば、次のように直してもいいでしょう。

リスト : 配列データの表示 (3)

for (my $i = 0; $i < @m; $i++) {
   my $a = $m[$i];
   for (my $j = 0; $j < @$a; $j++) {
       print "$a->[$j] ";
   }
   print "\n";
}

変数 $a にリファレンスをセットします。このように、一時的な変数を使うことで、プログラムが簡単になる場合があります。

●値呼びと参照呼び

一般に、関数の呼び出し方には二つの方法があります。一つが「値呼び (call by value)」で、もう一つが「参照呼び (call by reference)」です。近代的なプログラミング言語では「値呼び」が主流です。C言語や Java は値呼びです。これに対し Perl の関数は「参照呼び」です。まず、C言語を例に値呼びと参照呼びの違いを説明しましょう。

値呼びの概念はとても簡単です。

(1) 受け取るデータを格納する変数 (仮引数) を用意する。
(2) データを引数に代入する。
(3) 関数の実行終了後、引数を廃棄する。

「仮引数」とは、関数がデータを受け取るための引数のことで、実際に渡される引数を「実引数」といいます。

値呼びのポイントは (2) です。データを引数に代入するということは、データのコピーが行われるということです。変数に格納されている値そのものを関数に渡すので、「値渡し」とか「値呼び」と呼ばれます。

値呼びは単純でわかりやすいのですが、呼び出し先 (caller) から呼び出し元 (callee) の局所変数にアクセスできると便利な場合もあります。仮引数に対する更新が直ちに実引数にも及ぶような呼び出し方が「参照呼び」です。

たとえば、引数を足し算する関数 plus をC言語でプログラムしてみましょう。

リスト : 引数を足し算する

int plus(int x, int y)
{
  return x + y;
}

最初の int は関数 plus が返す値のデータ型を表します。関数名 plus の後ろのカッコ ( ) に仮引数を定義します。この場合、引数の x と y は整数値を受け取ります。次に、plus を呼び出す関数 foo を作ります。

リスト : 関数 plus を呼び出す

int foo()
{
  int x, y, z;
  x = 10;
  y = 20;
  z = plus(x, y);
  return z;
}

変数 x と y には 10 と 20 がセットされています。この値が関数 plus に渡されます。関数 plus を実行するときは、値を格納する引数をメモリから割り当て、そこに値を代入します。次の図を見てください。

plus が実行されるときに引数 x と y が用意され、そこに foo の変数 x と y の値を代入します。つまり、foo で定義された変数 x, y と plus の引数 x, y は、別のメモリに割り当てられているのです。したがって、関数 plus 内で変数 x, y の値を書き換えても、foo 内の変数である x と y に影響はありません。これは局所変数と同じ働きです。

関数 の回で説明しましたが、関数を部品のように使う場合、ある関数を呼び出したら、いままで使っていた変数の値が書き換えられていた、というのでは呼び出す方が困ってしまいますね。引数も変数ですから、ほかの処理に影響を及ぼさないように、局所変数として扱われるのです。

ところが、値呼びでは実現できない処理があるのです。たとえば、変数の値を交換する関数 swap を考えてみましょう。次のプログラムを見てください。

リスト : 値の交換 (間違った例)

void swap(int x, int y)
{
  int tmp = x;
  x = y;
  y = tmp;
}

int main()
{
  int x = 10;
  int y = 20;
  swap(x, y);
  printf("x = %d\ny= %d\n", x, y);
  return 0;
}

void は値を返さない関数であることを表します。関数 swap では引数 x, yの値を交換していますが、関数 main 内の変数 x, y の値は、swap を呼び出したあとでも交換されていません。main と swap の変数 x, y は別のメモリに割り当てられているので、swap 内の x, y を交換しても、main 内の x, y を交換することにはならないからです。関数が値呼びである以上、ほかの関数で定義された局所変数にアクセス [*4] することはできないのです。

関数 swap を実現するには、変数の値ではなく、変数そのものを渡します。C言語の場合、交換する変数のアドレスを「ポインタ」で渡せば実現できます。次の図を見てください。

swap の引数を「ポインタ」にすると、x, y には変数のアドレスが代入されます。そして、このアドレスに格納されている値を交換すればいいわけです。これは間接参照演算子 * を使えば簡単ですね。これで、渡されたアドレスに格納されている値を交換することができます。

プログラムは次のようになります。

リスト : 値の交換 (正解)

void swap(int *x, int *y)
{
  int tmp = *x;
  *x = *y;
  *y = tmp;
}

int main()
{
  int x = 10;
  int y = 20;
  swap(&x, &y);
  printf("x = %d\ny= %d\n", x, y);
  return 0;
}

関数 swap の引数はポインタとして宣言します。値の交換には間接参照演算子 * を使って、渡されたアドレスに格納されている値を交換します。x, y そのものを交換すると、x, y に格納されているアドレスを交換することになるので、swap は正常に動作しません。sawp を呼び出す main では、アドレス演算子 & を使って x, y のアドレスを求め、それを swap に渡します。これで変数の値を交換することができます。

-- note --------
[*4] 正確にいうと、レキシカルスコープを持つ局所変数のことです。ダイナミックスコープを持つ局所変数であれば、呼び出し先関数から呼び出し元関数の局所変数にアクセスすることができます。C言語の局所変数はレキシカルスコープです。

● Perl の関数は参照呼び

Perl の場合、実引数は配列 @_ に格納されて渡されますが、実は「参照渡し」されています。したがって、値を交換する swap は簡単に作ることができます。次のプログラムを実行してください。

リスト : 参照呼び

use strict;
use warnings;

sub swap {
    my $temp = $_[0];
    $_[0] = $_[1];
    $_[1] = $temp;
}

my $a = 10;
my $b = 20;

print "$a, $b\n";     # 10, 20 と表示
swap($a, $b);
print "$a, $b\n";     # 20, 10 と表示

変数 $a と $b の値が交換されています。配列 @_ の要素は $a と $b を指し示していて、$_[0] と $_[1] で $a と $b にアクセスすることができるのです。@_ には実引数へのリファレンスが格納されていて、@_ の要素にアクセスするとデリファレンスされる、と理解してもいいでしょう。もっとも、@_ からリファレンスを取り出すことはできません。@_ は Perl 5 からサポートされたリファレンスとは違う、ということを忘れないでください。

それでは、関数を値呼びしたい場合はどうするのでしょうか。いままでたくさんのプログラムを作ってきたので、皆さんもうご存じですね。@_ を局所変数に代入すればいいのです。このことにより、実引数の値は局所変数にコピーされ、@_の値を変更しなければ、Perl でも値呼びを実現できます。実際に、@_ を変更する関数を作ることは、Perl でもめったにありません。ほとんどの関数が値呼びで使われます。そして、その方が見通しの良いプログラムを作ることができます。


初版 2015 年 4 月 26 日
改訂 2023 年 3 月 19 日

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

[ PrevPage | Perl | NextPage ]