北海道苫小牧市出身の初老PGが書くブログ

永遠のプログラマを夢見る、苫小牧市出身のおじさんのちらしの裏

use constant した定数を実行時に書き換えるのは難しい

恥ずかしながら今日まで知らなかったんすっかり失念してたですが、要はラクダ本第3版の6.4.1 定数関数のインライン化の内容です。

過ちを犯した

use constant は関数と等価であることは有名です。それを利用して、定義した内容を実行時に別の値にしたくて以下のコードを書いたんですが、駄目でした。

use strict;
use constant HOGE => 'A';

no warnings 'redefine';
*HOGE = sub () {'B'};

print HOGE, "\n";

これは A と出力されます。

理由

Deparseするとわかります。

% perl -MO=Deparse test.pl
use constant ('HOGE', 'A');
no warnings;
use strict 'refs';
*HOGE = sub () { 'B' }
;
print 'A', "\n";
test.pl syntax OK

見事にインライン展開されてますね。これは、

  1. use constant HOGE ... は sub HOGE (){...} と等価
  2. プロトタイプ() を持つ関数はインライン化される*1

と言う仕様によります。よって、コンパイルが終わって実行フェーズに移ってしまってからでは、定数を書き換えることは不可能です。

それでも書き換えたい

以下のようにコンパイル前に書き換えるしかありません。Bと出力されます。

use strict;
use constant HOGE => 'A';

BEGIN{
	no warnings 'redefine';
	*HOGE = sub () {'B'};
}
print HOGE, "\n";

しかし、モジュール構成が複雑になって来ると、どのタイミングでどの定数を利用する部分がコンパイルされるかを予測するのは非常に難しくなってきます。起動スクリプトの最初の最初で使う以外では、決してお勧めできる方法ではありません*2

ちなみに、一応no warnings 書いてみてますが、Constant subroutine ... redefined のwarningはこれでは消えません。危険性を考えると当然かと思います。

追記

実は過去に全く同じことやってましたww 人間って忘れる生き物だなあ。

*1:厳密には、当然もっと条件があります

*2:と言っても、書き換えるにはその前に定数定義部分をコンパイルする必要があるので、そこが別の書き換えたい定数を使ってたりするとうまく行かない。