スキップしてメイン コンテンツに移動

仕事で使わない Perl コーディングテクニック

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)

ちなみに existsdelete 演算子などと同様に配列にも一般化されている:

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::mapoperator[] なんかが典型である。

組み込み関数で左辺値を返す典型は 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 も併用して値の検査を型制約に任せても良い。

コメント

このブログの人気の投稿

Perl 5 to 6 - コンテキスト

2011-02-27: コメント欄で既に改訂された仕様の指摘がありました ので一部補足しました。 id:uasi に感謝します。 これはMoritz Lenz氏のWebサイト Perlgeek.de で公開されているブログ記事 "Perl 5 to 6" Lesson 06 - Contexts の日本語訳です。 原文は Creative Commons Attribution 3.0 Germany に基づいて公開されています。 本エントリには Creative Commons Attribution 3.0 Unported を適用します。 Original text: Copyright© 2008-2010 Moritz Lenz Japanese translation: Copyright© 2011 SATOH Koichi NAME "Perl 5 to 6" Lesson 06 - コンテキスト SYNOPSIS my @a = <a b c> my $x = @a; say $x[2]; # c say (~2).WHAT # Str() say +@a; # 3 if @a < 10 { say "short array"; } DESCRIPTION 次のように書いたとき、 $x = @a Perl5では $x は @a より少ない情報—— @a の要素数だけ——しか持ちません。 すべての情報を保存しておくためには明示的にリファレンスを取る必要があります: $x = \@a Perl6ではこれらは反対になります: デフォルトでは何も失うことなく、スカラ変数は配列を単に格納します。 これは一般要素コンテキスト(Perl5で scalar と呼ばれていたもの)及びより特化された数値、整数、文字列コンテキストの導入によって可能となりました。無効コンテキストとリストコンテキストは変更されていません。 特別な構文でコンテキストを強制できます。 構文 コンテキスト ~stuff 文字列 ?stuff 真理値 +stuff ...

LIBLINEAR 2.41 で One-class SVM が使えるようになったので Perl から触ってみよう

改訂 (Sep 15, 2020): 必要のない手順を含んでいたのでサンプルコードと記述を修正しました。 CPAN に Algorithm::LibLinear 0.22 がリリースされました (しました。) 高速な線形 SVM およびロジスティック回帰による複数の機械学習アルゴリズムを実装したライブラリである LIBLINEAR への Perl バインディングです。 利用している LIBLINEAR のバージョンが LIBLINEAR 2.30 から LIBLINEAR 2.41 に上がったことで新しいソルバが追加され、One-class SVM (OC-SVM) による一値分類が利用可能になっています (しました。) OC-SVM って何 一値分類を SVM でやること。 一値分類って何 ある値が学習したクラスに含まれるか否かを決定する問題。 HBO の「シリコンバレー」に出てきた「ホットドッグ」と「ホットドッグ以外」を識別するアプリが典型。「ホットドッグ以外」の方は犬でも神でも一つの指輪でも何でも含まれるのがミソ。 二値分類の場合正反両者のデータを集める必要があるのに対して、一値分類の学習器は正例データのみしか要求しない (ものが多い。) 主な用途は外れ値検出で、もちろんホットドッグやホットドッグ様のものを検出したりもできる。 使い方 手順自体は他の二値ないし多値分類問題と同じです。つまり、 訓練パラメータを決めて 訓練データセットで訓練して テストデータセットで確度を検証して 十分良くなったらモデルを保存する といういつもの流れ。 訓練パラメータ use 5.032 ; use Algorithm::LibLinear ; my $learner = Algorithm::LibLinear ->new( epsilon => 0.01 , nu => 0.75 , solver => ' ONECLASS_SVM ' , ); solver => 'ONECLASS_SVM' が一値分類用のソルバです。LIBLINEAR の train コマンドで言うところの -s 21 。 OC-SVM の良いところは (ハイパー)...

Perl 5 to 6 - ツイジル

これはMoritz Lenz氏のWebサイト Perlgeek.de で公開されているブログ記事 "Perl 5 to 6" Lesson 15 - Twigils の日本語訳です。 原文は Creative Commons Attribution 3.0 Germany に基づいて公開されています。 本エントリには Creative Commons Attribution 3.0 Unported を適用します。 Original text: Copyright© 2008-2010 Moritz Lenz Japanese translation: Copyright© 2011 SATOH Koichi NAME "Perl 5 to 6" Lesson 15 - ツイジル SYNOPSIS class Foo { has $.bar; has $!baz; } my @stuff = sort { $^b[1] <=> $^a[1]}, [1, 2], [0, 3], [4, 8]; my $block = { say "This is the named 'foo' parameter: $:foo" }; $block(:foo<bar>); say "This is file $?FILE on line $?LINE" say "A CGI script" if %*ENV.exists('DOCUMENT_ROOT'); DESCRIPTION いくつかの変数にはツイジルという第2のシジルがあります。これは基本的にはその変数が「普通」ではないということです。違いはいくつかあり、例えばスコープの違いなどです。 オブジェクトのパブリックな属性とプライベートな属性がそれぞれ . と ! というツイジルを持つことは既に紹介しました; それらは通常の変数ではなく self に結びつけられています。 ツイジル ^ はPerl5で例外的に扱われていたケースを一般化します。次のように書けます # 注意: Perl5のコードです sort ...

多分週刊チラシの裏 (Sep 28 - Oct 04, 2020)

Chrome Web Store が有料 Chrome 拡張の取扱を終了 Chrome Web Store で提供されている有料 Chrome 拡張及びアプリ内課金 API の両方が 2021 年 1 月いっぱいで廃止される。 開発者はそれまでに代替となるサードパーティの課金 API に移行し、購入済ライセンスの移行手段も用意する必要がある。 この決定の発表時点で新規の有料ないしアプリ内課金のある Chrome 拡張の新規登録は終了している。実際のところ 2020 年 3 月時点で既に「一時的に」停止されており、その措置が恒久化されただけとの由。 シェルスクリプティングには長いオプションを使え 「短いオプション (e.g., -x ) はコマンドライン上での略記である。スクリプトにおいては自分や将来の同僚のためにも長いオプション (e.g., ---do-something ) を与える方が理解が容易だろう」という主張。 異論の余地なく正論である。 CobWeb - COBOL to WebAssembly Compiler COBOL から WebAssembly へのコンパイラ。いやマジで。 Cloudflare が何を思ったか同社のサーバレス環境である Workers に COBOL 対応を追加した際 の成果物である。 COBOL から C へのトランスレータである GNU COBOL と C コードをコンパイルして WebAssembly を出力する Emscripten から成っており、他の言語に比べて軽量なバイナリを生成するとのこと。 「ウチではそんな風にはやらないんだ (“We don’t do that here”)」 昨今ソフトウェア開発のコミュニティでも Code of Conduct を用意するところが増えてきたが、コミュニティの文化を明文化するのは難しい。 長大な「べからず集」は息苦しいし、肯定的なガイドラインは時に抽象的で実効的に使えない。問題となるようなふるまいの動機が善意であった場合は特にそうだ。 仮に優れたガイドラインがあっても、それに基いて人を実際に咎めるのは骨が折れることである。初中やればコミュニティ内でも疎まれる。 話の分かる相手ならそれでもまだ説得する意義もあるが、Web 上の対話で当事者双方が納得し合っ...

Perl 5 to 6 - 遅延性

これはMoritz Lenz氏のWebサイト Perlgeek.de で公開されているブログ記事 "Perl 5 to 6" Lesson 12 - Laziness の日本語訳です。 原文は Creative Commons Attribution 3.0 Germany に基づいて公開されています。 本エントリには Creative Commons Attribution 3.0 Unported を適用します。 Original text: Copyright© 2008-2010 Moritz Lenz Japanese translation: Copyright© 2011 SATOH Koichi NAME "Perl 5 to 6" Lesson 12 - 遅延性 SYNOPSIS my @integers = 0..*; for @integers -> $i { say $i; last if $i % 17 == 0; } my @even := map { 2 * $_ }, 0..*; my @stuff := gather { for 0 .. Inf { take 2 ** $_; } } DESCRIPTION Perlプログラマは怠けがちです。彼らが使うリストも。 ここで怠惰という言葉が意味するのは、評価が可能な限り遅延されるということです。 @a := map BLOCK, @b のようなコードを書いたとき、ブロックは一切実行されません。 @a の要素にアクセスしようとしたときだけ map は実際にブロックを実行し、必要とされる分だけ @a を埋めます。 代入ではなくバインディングを使っていることに注意して下さい: 配列への代入は先行評価を強制することがあります(コンパイラがリストの無限性に気づかない限り; 無限リスト検出の詳細はまだ固まっていません)。 バインディングはそのようなことがありません。 遅延性は無限リストの取り扱いを可能にします: 引数すべてに操作を行うようなことさえしなければ、評価された要素に必要なだけのメモリしか必要としません。 しかし落とし穴があります: 長さの...