Everyone knows that debugging is twice as hard as writing a program in the first place. So if you're as clever as you can be when you write it, how will you ever debug it? -- Brian Kernighan
(まずもって、デバッグがプログラミングの倍も難しいことは衆知である。だからもしあなたが能う限りの知力でコードを書いたとしたら、どうやってデバッグするのか?)
言語のマイナ機能とか関数のデフォルトの挙動なんかを駆使してプログラムを書くと未来の自分や同僚が困るので、普段はいくらか制限して書くと思う。 僕は Perl 5 だとライブラリはともかくアプリケーションのコードは「続・初めてのPerl (Intermediate Perl)」程度の知識を前提して書いていた。
要するに意図的に低俗なコードを書いているわけで面白くない。 だいたい頭の内にあるボンヤリとした「Perl 的な書き方」が実際に動くのがこの言語の良いところで、昔2ちゃんねるで見かけた「キモいが矛盾していない、それが Perl」というコメントはけだし慧眼だったと思う。
そんなわけでマイナであったり環境の都合で自制していた言語機能をいくつか紹介したい。
メソッドの事前解決
Perl 5 のメソッド呼出しは遅い。まずもってメソッドの解決が遅い。繰り返し同じメソッドを呼ぶときはメソッドを一度だけ解決してそれを直接呼び出したい。要するに Objective-C で selector を取得してそれを実行するようなことをやりたい。
名前のせいで述語と勘違いしがちだが UNIVERSAL::can
はまさにこのメソッド解決を行う。例えば my $do_foo = $obj->can('do_foo');
とすれば $obj
が属するパッケージないしその親クラスに存在する do_foo
メソッドへのリファレンスが得られる。存在しない場合は undef
である。
そうとなれば後は簡単で、メソッドは第一引数にインスタンスないしクラス名を受けるただのサブルーチンなので、$do_foo->($obj, ...)
と呼び出せば動くことは動く。 しかしこれは不恰好である。抽象が破れているではないか。何故メソッド呼び出しだったものを関数呼び出しに書き換えなければならないのか。
実はメソッド呼び出しの矢印演算子の右辺には CodeRef が置ける。この場合メソッド解決は省略され、単に左辺を第一引数として右辺の CodeRef が呼ばれる。つまり解決済みの $do_foo
メソッドを呼び出すには $obj->$do_foo(...)
とすれば良い。
この事実は perlop と perlobj に載っているが、そもそもメソッド名は裸のワードであるから都合の良いときはほぼいつでもシンボリック・リファレンスで与えられることを我々は知っている。そうなればハード・リファレンスに拡張されることも自然に類推できる。
Perl 5.24 にて実験を行った。簡単なカウンタを作り、百万回 incr
メソッドを呼んで数値をインクリメントする。"regular" は毎回メソッド解決を行い、"resolved" は can
で解決済みのメソッドを使う:
#!/usr/bin/env perl
use strict;
use warnings;
use Benchmark qw/cmpthese/;
package Counter {
sub new {
my ($class, $initial) = @_;
bless \$initial => $class;
}
sub count {
my ($self) = @_;
$$self;
}
sub incr {
my ($self) = @_;
++$$self;
}
}
cmpthese(
-5,
+{
regular => sub {
my $counter = Counter->new(0);
$counter->incr for 1 .. 1_000_000;
},
resolved => sub {
my $counter = Counter->new(0);
my $incr = $counter->can('incr');
$counter->$incr for 1 .. 1_000_000;
},
},
);
結果は以下の通りで、解決済みのメソッドを使う方が20ポイントほど高速であった:
bash-3.2$ perl ~/toybox/sel.pl
Rate regular resolved
regular 3.73/s -- -18%
resolved 4.56/s 22% --
ブロックの last
/ redo
ブロックは新しい字句的スコープを導入する。これはガードオブジェクトと組み合わせて確実にリソースを開放したいときなどに便利だが、それだけに留まらない。
ブロックは「一回こっきり実行されるループ」と見なして良い。つまりループ制御コマンドの last
/redo
はループでないただのブロックでも使用できる (next
も使えるが last
と同じになるので意味はない)。
これは途中で失敗したときにやり直す処理を書くのに便利である:
my @resources = (...);
my @locks;
my $retries = 0;
my $lock_succeed = 0;
TRY_LOCKS:
{
last if $retries >= 3;
for my $resource (@resources) {
my $lock = try_lock($resource);
unless ($lock) {
++$retry;
@locks = ();
redo TRY_LOCKS;
}
push $locks, $lock;
}
$lock_succeed = 1;
}
if ($lock_succeed) { ... }
Key-Value ハッシュスライス
これは Perl 5.8 フリーな環境では仕事で使っている人もいるかも知れない。
perldelta をちゃんと毎回読んでいる人には衆知だが Perl 5.20 からの新機能。Perl 5.10 の //
defined-or 演算子や Perl 5.14 の /r
非破壊的置換修飾子以来のキラー機能だと思う。 要するにハッシュの一部だけを抜き出して偶数サイズのリストにする機能で、構文は大方の想像通り:
my %source = (foo => 1, bar => 2, baz => 3);
# equivalent: map { $_ => $source{$_} } qw/foo bar/;
my %projected = %source{qw/foo bar/}; # (foo => 1, bar => 2)
ちなみに exists
や delete
演算子などと同様に配列にも一般化されている:
my @source = 'a' .. 'z';
# equivalent: map { $_ => $source[$_] } [3, 4, 5]
my %projected = %source[3, 4, 5]; # (3 => 'd', 4 => 'e', 5 => 'f')
少々異和感を覚えるが、代入の両辺で sigil が揃う点で一貫しているし慣れの問題だろう。
左辺値の返却
関数やメソッドは左辺値を返せる。左辺値であるからにはつまり代入できる。
C++ でも lvalue 参照を使うと同じことができて、std::map
の operator[]
なんかが典型である。
組み込み関数で左辺値を返す典型は substr
/ vec
/ splice
である。 perldoc -f substr
を見るとプロトタイプは次のようになっている:
substr EXPR,OFFSET,LENGTH,REPLACEMENT substr EXPR,OFFSET,LENGTH substr EXPR,OFFSET
この内4引数版に代入するのはナンセンスなのでエラーになるが、2引数版と3引数版は第一引数 (EXPR) が左辺値であれば代入ができる:
my $str = 'bar';
substr($str, 2, 1) = 'z'; # $str eq 'baz'
つまり右辺の値は4引数版の第四引数に相当する。substr
は第一引数への一種のビューを提供しているとも見なせるが、実際にはそれ以上のことを行う。代入される文字列の長さが substr
で得られた文字列と異なる場合は元の文字列が伸縮する。例えば空文字列を代入することで部分文字列を削除できる。
ちなみ代入式全体の値は元の (置換された) 文字列である。上記の例であれば 'r'
が返る。
同様にvec
はビット列に対してビット列を代入することができるし、splice
は配列に対してリストを代入できる。
また少し意外なものとしては keys
も左辺値を返す。これは C++ の STL コンテナにある reserve
メンバ関数に似ていて、事前にハッシュの記憶スロットを確保しておくのに使える:
my %code_map;
keys(%code_map) = 26;
$code_map{+chr} = $_ for 65 .. 90;
蛇足として keys
自体は Perl 5.12 から配列に対しても使えるが、この場合左辺値としては使えない。 配列の事前確保は特殊変数 $#array
への代入で行えるが、こちらは伸びた領域が実際に undef
で埋まってしまうので元の要素数に戻す一手間が要る:
my @char_map;
$#char_map = 26 - 1; # Initialized with (undef) x 26
@char_map = (); # もう一回空にするが、確保されたスロットはそのまま
push @char_map, chr for 65 .. 90;
さてユーザ定義する場合だが、lvalue
属性がついたサブルーチンの最後に評価される式が左辺値であれば良い。メソッドの例を示す:
package Foo;
sub new {
my ($class) = @_;
bless +{ foo => '' } => $class;
}
sub foo :lvalue {
my ($self) = @_;
$self->{foo};
}
package main;
use 5.024;
my $obj = Foo->new;
$obj->foo = 'bar';
say $obj->foo; # 'bar'
属性 (attributes) を忘れている人も多いと思うが、要するに Java でいうアノテーションである。Catalyst なんかで見たことがあるだろう。
うっかり値を return
してしまうと左辺値にならないことに注意が必要である。(return $x) = 42;
はナンセンスだ。
ところでアクセサがオブジェクトの状態を左辺値として返すのはカプセル化の観点からはあまりよろしくない。代入される値を検査できないのでオブジェクトの状態が保障できないからである。 これを避けるには代入操作にフックして検査をすれば良い。C++ なら返す型の代入演算子 (operator=
) をオーバーライドするが、Perl ではスカラの代入演算をオーバーライドする。つまり tie
するのである:
package Bar;
use strict;
use warnings;
sub new {
my ($class) = @_;
bless +{ bar => 0 } => $class;
}
sub bar :lvalue {
my ($self) = @_;
tie my $v, 'Bar::NumberField' => $self->{bar};
$v;
}
package Bar::NumberField;
use strict;
use warnings;
use Carp ();
use Scalar::Util qw/looks_like_number/;
# $_[1] は tie に渡された引数 (e.g., $self->{bar}) のエイリアスなので、
# リファレンスを保存しておくことで後で書き換えられるようにする。
sub TIESCALAR { bless \$_[1] => $_[0] }
sub FETCH { ${$_[0]} }
sub STORE {
my ($self, $value) = @_;
unless (looks_like_number $value) {
Carp::croak("Seems like not a number: $value");
}
$$self = $value;
}
package main;
use strict;
use warnings;
use feature qw/say/;
my $bar = Bar->new;
$bar->bar = 42;
say $bar->bar; # 42
$bar->bar = 'blah blah blah'; # Error.
この例だとやりたいことに比べて労力が馬鹿げているが、ともかく tie
された変数を返すことで substr
のような組み込み関数と同じ挙動をユーザ定義できるということに意義がある。 実際的には Sentinel のようなライブラリを使うことでシンプルに書ける。なんなら Moose も併用して値の検査を型制約に任せても良い。
コメント
コメントを投稿