Perlにおける定数宣言(constant or Readonly or Attribute::Constant)

正直言いますと、個人的には定数って殆ど使わないんですよね。

昔はちょこちょこ使ってたんですが、最近は定数となりうる物は基本的にYAML等外部ファイルに持たせて変更可能にすることが多いのでまったく使ってません。

ですがそれでもPerlで定数を使いたい時にはどーすんの?っていう時のために軽くここにまとめておきます。


言わずもかなPerl5.8にはconstantという定数を定義できる標準モジュールがあります。

use strict;
use constant PI => 3.14;

# 表示
print PI; # 3.14

# 上書きしようとすると・・・
PI = 3; # Can't modify constant item in scalar assignment

とこのように上書きしようとするとエラーを吐いて死にます。

しかも実行時ではなく、コンパイル時にエラーを吐きます。

実はこのconstantで宣言された定数というのは、単なる関数なのです。

つまりconstantとは下記のような関数宣言とほぼ同義です。

use strict;
sub PI () { 3.14; }

print PI;

PI = 3;

関数に対して代入しようとしてるわけですから、コンパイル時にエラー(lvalueという仕組みもありますがここでは割愛)となるわけです。

さてしかしながらこのconstant、見ての通り単なる関数なので色々と不便なのです。

まず文字展開ができない。

use strict;
use constant ROWS => 10;

print "row is ".ROWS.".";

とこのように非常にめんどくさい。

次に、配列やハッシュの定数を定義できない。

いや正確にはリファレンスにすれば定義できる。

しかしながらリファレンスを定数にしてもそのアドレスそのものは上書きできないが、参照しているデータは上書きできるので注意が必要なのです。

use strict;
use constant DATA => [ 10, 20, 30 ];

print DATA->[0];


DATA = [ 10, 20, 100 ]; # これは定数なのでエラーになるが

DATA->[2] = 100; # これはあくまでリファレンスが参照している先のデータなのでエラーにはならない

そしてリファレンスを定数にした場合、とてもデリファレンスがしにくい。

constantで定義した定数が単なる関数であるということを知っているならば躓くことは少ないが、良く知らずにconstantを利用していた場合、どうやってループ処理を書けばいいのか途方に暮れる事になるだろう。

use strict;
use constant DATA => [ 10, 20, 30 ];

for ( @{DATA} ) {} # この書き方はエラーとなる

for ( @{DATA()} ) {} # DATAは関数だよと明示してやる必要がある

ちなみに余談だが、関数であると明示する際に「DATA()」ではなく、「&DATA」と書いても同じ結果が得られる。

ただし後者のように書いた場合は関数がインライン展開されなくなるので定数としての旨みが減ってしまう。

インライン展開されてるかどうかは下記の検証プログラムをB::Deparseで解析することによって見ることができる。

# test.pl
use strict;
use constant NUM => 10;

print NUM;
print NUM();
print #

-------------------------------
$ perl -MO=Deparse test.pl
use constant ('NUM', 10);
use strict 'refs';
print 10;
print 10;
print #

見ての通り「&」で呼び出した関数は展開されてないことがわかる。


話しを戻すと、constantは以上のような問題点を抱えているというわけなのです。

そこでReadonlyというモジュールの登場です。

百聞は一見に如かず。下記の例をば見てください。

use strict;
use Readonly;

Readonly my $ROWS => 10;
Readonly my @DATA => (10, 20, 30);

print "row is $ROWS.";

for my $item (@DATA) {
    print $item;
}

$ROWS    = 20;  # エラー
$DATA[2] = 100; # エラー

と、このようにReadonlyを使って定義された定数はあくまで変数なので文字列展開や配列の定数など(もちろんハッシュの定数も)を作ることが可能です。

また実体はmy変数なので、関数形式の定数と違いレキシカルスコープな定数を作れるという利点もあります。

use strict;
use Readonly;

while (1) {
    Readonly my $DATA = get_data();

    # ループが終わるまで$DATAが変更できないことが保証される。

}

さてさてここまで書いてると、なんだなんだ良い事ずくめじゃないかという結論になりそうですが、Readonlyにはデメリットも存在します。

まず遅いというのがあげられるでしょう。constantの場合は単なる関数で且つインライン展開されるので速いのですが、Readonlyはtieを使って実装されているため速度の低下は否めません。Readonly::XSというのもありますがそれでもやっぱり遅い。

また、定数を上書きした場合、コンパイル時ではなく実行時にエラーが検出されるという点も大きな違いです。



まぁそれでも基本的にはReadonlyの方が堅牢で美しいと思うので個人的にはReadonlyの方をオススメしていきたいですね。Perlベストプラクティスでもそう言ってるしw

と、いうところでPerlにおける定数宣言についてのお話は終わりにします。








・・・と言いたいところですが、実はあまり一般的に使われてはないと思うものの、Readonlyより高速な変数ベースの定数宣言するモジュールが登場してます。

それがAttribute::Constant(or Data::Lock)というモジュールです。

モジュールの作者は弾さんです。詳しい説明はこちら→404 Blog Not Found:perl - Attribute::Constant - Another Way to Make Read-only Variables

Readonlyのようにtieを使うのではなく、Internals::SvREADONLYを利用して書き込み制限をかけているところが斬新です。

またAttribute::Constantという名前から分かるとおり、定数をアトリビュートで定義できるところがなかなかカッコいいですね。

use strict;
use Attribute::Constant;

my $ROWS : Constant(10);
my @DATA : Constant(10, 20, 30);

print "row is $ROWS.";

for my $item (@DATA) {
    print $item;
}

またアトリビュートが肌に合わないという人用(?w)にData::Lockというモジュールももれなく付いてきます。

こちらはReadonlyと同じようなインターフェイスで定数を定義できます。

use strict;
use Data::Lock qw(dlock);

dlock my $ROWS = 10 ;
dlock my @DATA = (10, 20, 30);

print "row is $ROWS.";

for my $item (@DATA) {
    print $item;
}

このように変数を書き込み不可にするdlockが提供されます。そして面白いことに書き込み可に戻すdunlockも存在します。

でも後から定数の変更手段が用意されてるのであれば定数である意味も意義も失いそうですが・・・。

だからモジュールのネーミングがData::Lockなんでしょうね。一時的に書き込みを出来なくするモジュールという意味で。


まとめ

ここらでまとめを。

  • constantの利点
    • Perl5.8標準モジュール
    • 定数の実体はただの関数なのでインライン展開されて速い
    • 上書きした場合、コンパイル時に検出できる
  • constantの欠点
    • 文字展開できない
    • 配列やハッシュの定数をリファレンス以外で定義できない
    • デリファレンスがしにくい
    • レキシカルスコープな定数を作れない
  • Readonlyの利点
    • 配列やハッシュの定数を定義できる
    • 文字列展開できる
    • レキシカルスコープな定数を定義できる
  • Readonlyの欠点
    • Readonly::XSを使っても遅い
    • 標準モジュールに含まれていない(Perl5.10からは標準モジュール入り)
    • 上書きした場合、実行時にしか検出できない
  • Attribute::Constant(or Data::Lock)
    • 利点欠点ともにReadonlyとほぼ同じだが、Readonlyよりも速度が速い

結局どれ使えばいいんだよ

初めに言ったとおり僕は定数を使わないのでんなこと言われたって知らないお。

しいて言うなら

our $NUM = 10;

かなw(全然定数じゃない件)




最後に

参考までにベンチマークを置いておきます。

use strict;
use warnings;
use Benchmark qw(cmpthese timethese :hireswallclock);
use Attribute::Constant;
use Data::Lock qw(dlock);
use Readonly;

use constant NUM1 => 10;

sub NUM2 () { 10; }

Readonly my $NUM3 => 10;

my $NUM4 : Constant(10);

dlock( my $NUM5 = 10 );

my $NUM6 = 10;
Internals::SvREADONLY( $NUM6, 1 );

cmpthese(100000,{
    constant => sub {
        for (0..NUM1) {}
    },
    subconstant => sub {
        for (0..NUM2) {}
    },
    noinline => sub {
        for (0..&NUM2) {}
    },
    readonly => sub {
        for (0..$NUM3) {}
    },
    attribute => sub {
        for (0..$NUM4) {}
    },
    dlock => sub {
        for (0..$NUM5) {}
    },
    internals => sub {
        for (0..$NUM6) {}
    },
});


__END__

                Rate readonly noinline dlock constant subconstant internals attribute
readonly    106724/s       --     -75%  -77%     -77%        -77%      -77%      -77%
noinline    427350/s     300%       --   -6%      -6%         -6%       -7%       -7%
dlock       454545/s     326%       6%    --      -0%         -0%       -1%       -1%
constant    456621/s     328%       7%    0%       --         -0%       -0%       -0%
subconstant 456621/s     328%       7%    0%       0%          --       -0%       -0%
internals   458716/s     330%       7%    1%       0%          0%        --       -0%
attribute   458716/s     330%       7%    1%       0%          0%        0%        --

上位5つの成績はもう殆ど同着で誤差程度なのでベンチ取り直すと逆転したりします。

ちゅーかAttribute::ConstantってばReadonlyと同等のことできるくせにほんと早いなw

あとnoinlineを見る限り、やはりインライン展開させないと若干ながら速度の低下が見られますね。

こうやって全体で見てみるとやっぱりReadonlyはちょっと遅めです。