18
14

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Perl のグローバル変数 $a と $b の詳解

Last updated at Posted at 2016-05-23

先日「Perl のグローバル変数やレキシカル変数について - Qiita」という記事を書きました。

グローバル変数がパッケージと紐付いているという話までくると、型グロブといったリファレンス前夜の Perl 4 の時代に行われていたグローバル変数によるプログラミング手法まで話が広がるのですが、少々長くなりすぎることもあり、以前の記事はあの分量に収めました。

型グロブの詳解も含めた一般的な Perl のグローバル変数についてのお話はいつかするとして、グローバル変数という話で忘れられないのは sort 関数で使われる $a $b という変数です。この記事では sort 関数の基礎や $a$b の正体を解明しつつ、sort 関数のような $a$b を受け取って何かするブロックを伴うような関数を自作する方法を解説します。

sort 関数と $a$b

(※ sort のブロックについてご存知の方はこのセクションは読み飛ばして下さって構いません。)

Perl で $a$b という変数が唐突に出てくるのは sort 関数 でしょう。

通常は文字列の辞書順の並べ替えの sort です。

sort1.pl
#!/usr/bin/perl

use strict;
use warnings;

my @words = ("foo", "bar", "buz", "quux", "squid");

for my $word (@words) {
    print "$word\n";
}
$ perl sort1.pl
foo
bar
buz
quux
squid

配列を参照するときに sort をつけると要素リストが辞書順に並べ替えられます。

sort2.pl
#!/usr/bin/perl

use strict;
use warnings;

my @words = ("foo", "bar", "buz", "quux", "squid");

for my $word (sort @words) {
    print "$word\n";
}
$ perl sort2.pl
bar
buz
foo
quux
squid

多くの場合はこれで満足ですが、数値の並べ替えをすると辞書順と数値の大きさ順の違いがわかります。

sort3.pl
#!/usr/bin/perl

use strict;
use warnings;

my @scores = (40, 50, 25, 70, 2, 14, 100, 90, 5, 45);

for my $score (sort @scores) {
    print "$score\n";
}
$ perl sort3.pl
100
14
2
25
40
45
5
50
70
90

sort には標準の辞書順で並び替えるという挙動を別の挙動にすることもでき、その指示を第一引数の前(間接オブジェクトスロット)に(無名関数のような)ブロックを置くことで行います。

多くの人は、これはイディオムとして丸暗記しているものです。

sort4.pl
#!/usr/bin/perl

use strict;
use warnings;

my @scores = (40, 50, 25, 70, 2, 14, 100, 90, 5, 45);

for my $score (sort { $a <=> $b } @scores) {
    print "$score\n";
}
$ perl sort4.pl
2
5
14
25
40
45
50
70
90
100

いい感じです。

とはいえ、突然出てきた { $a <=> $b } というものは何なのでしょうか。スペースシップ演算子 <=> は後述しますが、それ以外にも唐突に出てきた $a$b も不思議です。

sort 関数は、与えられた要素リストの中から2つの組み合わせを取り、その大小を検査します。この操作は 5 個の要素があればそれに応じた回数の比較を行い、並び替えの情報を集めます。

愚直に考えると $n$ 個であれば

{}_n\mathrm{C}_2 = \frac{n (n-1)}{2}

つまり $O(n^2)$ だけの回数比較操作は必要そうですが、Perl はもうちょっと良いアルゴリズムで $O(n \log n)$ 程度まで比較コストを抑えているそうです。

この時に、2つの変数の組 $a$b を取った時に sort 関数に並び替えの情報を与えるのが上記ブロック { $a <=> $b } なのです。この $a$b という変数名は予約されたグローバル変数です。Perl の一般的なサブルーチンのように、@_ で2つの値を与えられるのではないところが面白いですね。

このとき、sort 関数に与えるブロックは、与えられた $a$b に対して以下のように指示します。

  • $a$b がこの順番 ($a が左側) で並ぶ場合は -1 を返す
  • $a$b が並び替えの意味では同じ値の場合は 0 を返す
  • $a$b がこの逆の順番 ($b が左側) で並ぶ場合は 1 を返す

なので、数値の昇順(小さい数から大きい数へ)で並び替える場合には、以下の様なブロックを与えると良いのです。

my @sorted_scores =
sort {   $a <  $b ? -1 # この順序
       : $a == $b ? 0  # 等しい
       :            1  # この逆の順序(その他のケースなので条件なくてもいい)
     } @scores;

三項演算子の連結は慣れないと見づらいですが、上記の場合はインデントに従って「〜〜の場合は〜〜」と読んでいけばいいです。Perl の三項演算子がC言語と同様の右結合演算子なのでこのようなことができるのですが、詳しくは別の解説に譲ります。

ブロック内に改行を入れましたが、普通のサブルーチン定義同様、改行も入れられますし、セミコロン区切りで複数の文を書くこともできます(実際にはこの間接オブジェクトスロットの位置には同様の処理を行う通常のサブルーチンリファレンスを置くこともできます)。

ただ、数値の昇順というのは非常に多く行われますよね。いちいちこれを書くのはかなり骨が折れるため、上記のような計算をまとめて書くこことができるようにしたのがスペースシップ演算子とも呼ばれる <=> なのです。

my @sorted_scores =
sort { $a <=> $b } @scores;

逆順、数値の降順にしたい場合は $a$b を入れ替えて

my @sorted_scores =
sort { $b <=> $a } @scores;

とするか reverse 関数を使います。

my @sorted_scores =
reverse sort { $a <=> $b } @scores;

スペースシップ演算子で明示しているときは reverse を使わないほうが見やすいかもしれませんが、書き手の好みに依存する話題ですね。

この書き方を覚えておくと、例えば「与えられたファイル名のリストの拡張子になっている数字の順番で並べる」といったタスクもすぐに行うことができます。

# @filenames は "messages.20160501" みたいな文字列が入っている
my @sorted_filenames = sort {
  my ($an) = $a =~ /\.(\d+)$/;
  my ($bn) = $b =~ /\.(\d+)$/;
  $an <=> $bn;
} @filenames;

$a$bour せずに使える普通のグローバル変数

前述の通り $a$b はグローバル変数です。

以前書いた「Perl のグローバル変数やレキシカル変数について」という記事では、グローバル変数はパッケージと紐づく変数と説明しました。

$a$b は、私が真のグローバル変数と読んでいる $_ といった特殊記号との1文字変数と違い、あくまでもそのパッケージ内に所属しているグローバル変数です。そのことは簡単なプログラムで確認ができます。

set_special_globals.pl
#!/usr/bin/perl
# $_ is truly global variable
# $a is especially global varialble (for sort and some function). is not trully?

use strict;
use warnings;
use v5.10;

# initialize strictly
undef $_;
undef $a;
undef $main::_;
undef $main::a;
undef $Foo::_;
undef $Foo::a;

package Foo {
    sub set_a {
        $a = shift;
    }
    sub set_underline {
        $_ = shift;
    }
}

say "a is strictly undefined." if !grep { !defined } ($a, $main::a, $Foo::a);
say "_ is strictly undefined." if !grep { !defined } ($_, $main::_, $Foo::_);

say "set \$a at Foo";
Foo::set_a("foo");
say "a, main::a, Foo::a = " . ( join ",", map { $_ // "(undef)" } $a, $main::a, $Foo::a ) . "";

say "set \$_ at Foo";
Foo::set_underline("foo");
say "_, main::_, Foo::_ = " . ( join ",", map { $_ // "(undef)" } $_, $main::_, $Foo::_ ) . "";

Foo パッケージの中で $_$a を操作するというもの。これを実行すると以下のようになります。

$ perl set_special_globals.pl
set $a at Foo
a, main::a, Foo::a = (undef),(undef),foo
set $_ at Foo
_, main::_, Foo::_ = foo,foo,(undef)

Foo パッケージの中でパッケージ修飾の無い変数を操作したのに、$_$Foo::_ ではなく $main::_ が変更されました。これが $_ の真のグローバル変数であるゆえんなのですが、$a については普通に $Foo::a というグローバル変数が use strict 下で our 宣言なく使えるというものだと確認できます。

この観測は多くの初学者にはあまり関係なく、単純な Perl への興味と、以下のセクションの試みの布石となっています。

もっとも初学者にとって気をつけなければならないのは、$a$b という変数だけは use strict で補足することができない変数であるということです。

もっとも、「1文字変数を定義することは多くのプログラミングスタイルにおいて良くないもの」「$a という1文字変数をタイプミスするというケースがなさそう」という意味において、大きな影響はないものだといえますが、心の片隅に置いておくと役に立つことはあるでしょう。

ちなみに sort 関数の $a $b は「sort 関数は自身が使用されたパッケージのグローバル変数 $a $b に検査のための値を入れる」「use strict 時でも $a $b は暗黙で our 宣言されているかのように使うことが出来る」という意味でシステム固定のものです。これを別の変数名に変えることは通常できないはずです。

sort 関数の $a$b のようなものを自作関数にも導入したい

sort 関数のように第一引数の前の間接オブジェクトスロットにブロックを取る関数は自作することもできます。Perl において擬似的なブロック構文を導入したりといった DSL を作る場合の常套手段としても知られています。

そのような関数はプロトタイプ (&@) を付けて宣言をすることで、間接オブジェクトスロットに置かれたブロックがサブルーチンリファレンスの構造として第一引数 $_[0] で取得することができます。

下記の group_by 関数は、間接オブジェクトスロットの計算結果が eq において(ハッシュのキー名として)等しいもの同士でグルーピングをするものです。Ruby の Enumerable#group_by メソッドを真似しています。

# group_by { $_ % 2 } @list;
sub group_by (&@) {
    my $cb = shift;
    my @list = @_;
    my %res;
    for my $x (@list) {
        local $_ = $x;
        my $group = $cb->();
        push @{$res{$group} ||= []}, $x;
    }
    return %res;
}

use Data::Dumper;
print Dumper(group_by { $_ % 2 } (1..5));
$VAR1 = [
          '1',
          [
            1,
            3,
            5
          ],
          '0',
          [
            2,
            4
          ]
        ];

引数として $_[0] もしくは my $value = shift と書かないといけないところを、mapgrep のごとく最初から $_ に入っているという書き方です。local でグローバル変数 $_ の局所化をすることで、他方へ影響を与えること無く grep などの標準関数のような $_ による引数受け取りを行うことができます。

もっともこの例では、for で受ける変数 my $x 自体を省略してやることでリストの要素が $_ へ入るので、$_ のくだりを全部省くこともできるわけですが、その他の例にも適用可能なようにあえて冗長に書いています。

では、ブロックで2引数を取りたい場合、$a$blocal を使って同様に導入できるのでしょうか?

これについては、$_ が真のグローバル変数であり、$a$b は宣言しなくても使える暗黙のグローバル変数であるところの違いから、$_ のような簡単さで導入することはできません。

とはいえ本当に導入することはできないのでしょうか。実際、どのパッケージにおいても $a$bour 宣言せずともグローバル変数として(use strict 下での)エラーや警告を受けること無く参照することができます。こう考えることができると、caller で呼び出し元のパッケージ名を取って、そこに入れてやればいいということがなんとなく読めてきます。

では、2値を比較して同値である場合には真値を返すブロックで「同値関係」を示すことで、同値な要素ごとにグルーピングしてくれる quotient_set 関数を書いてみることにしましょう。これは数学の集合論における同値関係に付随する商集合の概念です。

以下の $a $b の導出方法は、List::MoreUtils::PPpairwise の実装を参考にしました。

package My::Math::Set;

use strict;
use warnings;

use List::Util qw(first);
use Exporter;

our $VERSION = "0.01";
our @EXPORT = ("quotient_set");

# quotient_set { "与えられた $a と $b はどうであれば等しいか" } @list;
# quotient_set { $a->{foo} eq $b->{foo} } @list;
# quotient_set { $a % 3 == $b % 3 } @list;
sub quotient_set (&@) {
    my $cb = shift;
    my @list = @_;
    # 呼び出し元のグローバル変数 $a $b の大元のグロブのリファレンスを取得
    my ($caller_a, $caller_b) = do {
        my $pkg = caller;
        no strict 'refs';
        (\*{"$pkg\::a"}, \*{"$pkg\::b"});
    };
    my @quotient_set = ( [shift @list] ); # 最初の元は比較しようがないので
    for my $e (@list) {
        # $e はどの集合の代表元と同値関係か
        # 同値関係の代表元がある集合族の元の集合に入れる
        my $equivalence_class = first {
            my $representative = $_->[0];
            # グロブリファレンスをデリファレンスしてグローバル変数のまま局所化
            local (*$caller_a, *$caller_b);
            # グロブに特定のシジルのリファレンスを入れると、そのシジルの値となる
            *$caller_a = \$e;
            *$caller_b = \$representative;
            $cb->();
        } @quotient_set;
        if ( $equivalence_class ) {
            push @$equivalence_class, $e
        } else {
            push @quotient_set, [$e];
        }
    }
    return @quotient_set;
}
setdiv1.pl
#!/usr/bin/perl

use strict;
use warnings;

use Data::Dumper;
use My::Math::Set; # パスの通っている場所に置く

print Dumper( quotient_set { $a % 2 == $b % 2 } (1..5) );
$ perl setdiv1.pl
$VAR1 = [
          [
            1,
            3,
            5
          ],
          [
            2,
            4
          ]
        ];

概要を解説します。

    # 呼び出し元のグローバル変数 $a $b の大元のグロブのリファレンスを取得
    my ($caller_a, $caller_b) = do {
        my $pkg = caller;
        no strict 'refs';
        (\*{"$pkg\::a"}, \*{"$pkg\::b"});
    };

このモジュールを use した元のパッケージ名をスカラーコンテキストの caller の返り値で得ます。それでパッケージ名の文字列を使って呼び出し元パッケージのグローバル変数 $a$b のパッケージ名修飾付きの変数名を作ります。ここで興味深いのは、参照としてスカラーリファレンスを取るのではなくグロブリファレンスを取ることです。…って、なんか他人事のように書いたのは、List::MoreUtils::PP::pairwise の実装がそうなっていたから :sweat_smile:

あとは、グロブリファレンスをグロブのシジルでデリファレンスした型グロブにしれっとスカラーリファレンスを代入することで、その型グロブが持つ名前のスカラー変数に値を割り当てることができます。

            # グロブリファレンスをデリファレンスしてグローバル変数のまま局所化
            local (*$caller_a, *$caller_b);
            # グロブに特定のシジルのリファレンスを入れると、そのシジルの値となる
            *$caller_a = \$e;
            *$caller_b = \$representative;
            $cb->();

もっとも、冒頭でグロブリファレンスではなくスカラーリファレンスを返し、割り当てはデリファレンスしたスカラー変数に値を代入することでも行えます。

    # 呼び出し元のグローバル変数 $a $b の大元のスカラーリファレンスを取得
    my ($caller_a, $caller_b) = do {
        my $pkg = caller;
        no strict 'refs';
        \(${"$pkg\::a"}, ${"$pkg\::b"}); # リファレンスの \ はカッコで分配法則が働くのでこう書いてもいい
    };
            $$caller_a = $e;
            $$caller_b = $representative;
            $cb->();

グロブリファレンスによる方法だと、コピーのコストが発生しない、つまりエイリアス変数のような実装が行えてパフォーマンス上の利点があるのかもしれません。

この方法を使うことで、sort 関数のような $a$b を受け取るブロックを持つ関数を書くことができました。ブロックを受け取る (&@) プロトタイプ宣言など、Perl で DSL やメタプログラミングを行うための手法も少しではありますが実践することができました。

型グロブといった言葉が出てきましたが、Perl 5 で登場したリファレンス前夜 Perl 4 時代のグローバル変数を扱う型グロブのお話は、別の記事で触れてみたいと思います。

18
14
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
18
14

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?