本連載では第一線のPerlハッカーが回替わりで執筆していきます。今回のハッカーはcharsbarこと石垣憲一さんで、テーマは
もとになるモジュールが存在している新機能
最近Perlに追加された新機能の中には、開発サイクルを速く回すためにCPANモジュールとしてプロトタイピングを行ったうえで、Perl本体に実験的な機能として移植し、正式な機能への昇格を待っているものがいくつかあります。本稿ではそのような実験的機能を紹介していきます。
なお、本稿のサンプルコードは基本的に執筆時の最新開発版であるPerl 5.
try
~catch
文 ──正しいエラー処理
Perlで例外を捕捉するには通常eval
文と$@
特殊変数を利用しますが、これらは気を付けて使わないと、
eval {
...
};
if ($@) {
...
}
これでもたいていは問題なく動作するのですが、$@
特殊変数はさまざまな事情で空になるので、無条件に信用することはできません。安全性を重視するなら、最低限eval
文の戻り値を見て成否を判定する必要があります。
eval { ...; 1; } or die "error!";
このような暗黙の了解を隠蔽するため、2009年にはTryCatch
とTry::Tiny
というモジュールが登場しました。
しかし、これらは新たな問題を生むことにもなりました。たとえば、TryCatch
モジュールにはPerlの内部に強引に手を入れて機能を追加するDevel::Declare
モジュールなどの実験的な依存モジュールが必要でした。また、軽量なTry::Tiny
モジュールは無名関数を第一引数に取ることでブロックらしい見た目を実現していたため、次の例が示すようにその中でreturn
文を実行しても無名関数を抜け出すことしかできないなどの制約がありました。
use Try::Tiny;
sub func {
try { return "success"; }
catch {
my $error = $_;
return "failure";
};
return "foo";
}
func(); # foo
このような制約が生まれたのは、当時のPerlには新しい構文を適切に追加するしくみが整っていなかったためです。その反省から、翌2010年のPerl 5.
そのあと、これらのAPIを利用するモジュールがいくつか登場したのですが、最終的にPerl本体の実装の基礎となったのは、2016年に実験が始まったSyntax::Keyword::Try
モジュールでした。その成果を受けて、2021年にリリースされたPerl 5.try
キーワードとcatch
キーワードが実験的機能としてPerl本体に取り込まれ、翌2022年のPerl 5.finally
キーワードも追加されています。
Syntax::Keyword::Try
モジュールやPerl本体が提供するtry
キーワードのあとに続くのは、無名関数ではなく本物のブロックです。よって、末尾のセミコロンは不要ですし、return
文も期待どおりの動作をします。
use v5.34;
use experimental qw(try);
sub func {
try { return "success"; }
catch ($e) { return "failure"; }
return "foo";
}
func(); # success
experimental
プラグマは見慣れないかもしれません。Perl 5.experimental
プラグマを使うことで、余計な警告を出すことなく実験的なキーワードを使えます。
defer
文 ──遅延実行するブロックを定義
2020年代に入ってPerlの開発体制が変わり、Syntax::Keyword::Try
モジュールの作者Paul Evans氏が開発の意思決定に積極的に関与するようになると、実験はさらに加速しました。
2021年にはSyntax::Keyword::Try
モジュールと同じくプラガブルキーワードAPIを利用したSyntax::Keyword::Defer
モジュールの実験が始まり、翌年のPerl 5.File::Temp
モジュールのようにガードオブジェクトを作成してDESTROY
時に後始末をしていた処理を素直に並べて書けます。
use v5.36;
use experimental qw(defer);
use File::Path;
sub func {
my $tmpdir = shift;
mkdir $tmpdir;
defer {
# エラー死しても実行されます
File::Path::remove_tree($tmpdir, {
safe => 1
});
}
...
}
後始末の意味ではEND
ブロックでも似たようなことはできますが、プログラムの最後に一度のみ実行されるEND
ブロックと異なり、defer
ブロックはスコープを抜けるたびに実行されるのが特徴です。
クラス ──まずは最低限のものを確実に
Perl本体に本格的なオブジェクト指向プログラミング用の機能を持たせようとする試みは、これまでにも何度か行われてきました。その最たる例は2000年に議論が始まり今はRakuと名を変えたPerl 6ですが、Perl 5の世界でもPerl 6の議論を踏まえてより優れたしくみを導入しようとする動きはありました。初期の試みの中で最も成功したのは、2006年に実装が始まったMoose
モジュールでした。
Moose
モジュールの時代
Moose
モジュールを使うと、たとえばname
という引数を持ち、挨拶ができるモジュールは次のように書けました。
package Person;
use v5.10;
use Moose;
use namespace::autoclean;
has 'name' => (is => 'ro');
sub say_hi {
my $self = shift;
say "Hi, I'm " . $self->name;
}
__PACKAGE__->meta->make_immutable;
機能を拡張したい場合は、extends
キーワードで親のモジュールを継承するか、再利用可能なサブルーチンをまとめたロールと呼ばれる特殊なモジュールをwith
キーワードで取り込むのでした。
package Author;
use v5.10;
use Moose;
use namespace::autoclean;
extends qw(Person);
with qw(Upload);
has 'pause_id' => (is => 'ro');
__PACKAGE__->meta->make_immutable;
Moose
モジュールは、重い、依存モジュールが多いなどの批判はあったものの、時流に乗って多くのユーザーを獲得しました。そのため、Perl本体にMoose
モジュールやその軽量版であるMoo
モジュールの同梱を望む声は根強くあったのですが、さまざまな事情で頓挫します。Test::More
モジュールの改善に合わせて、Moose
モジュールに近いMouse
モジュールを同梱したいという話も多くの抵抗に阻まれました。
2017年にはMoose
モジュールの原作者Stevan Little氏が新たに設計しなおしたMoxie
モジュールの実験を行ったのですが、これも受け入れられずに終わります。
Corプロジェクト
その歴史を踏まえてCurtis Poe氏が2019年に立ち上げたCorプロジェクトでは、いきなり完成品を目指すのではなく、まずは必要最小限のオブジェクト指向機能をPerlに組込み、安定させてから次に進むことを目標として仕様策定をはじめました。Paul Evans氏がその参考実装としてプラガブルキーワードAPIを利用したObject::Pad
モジュールの実験をはじめ、Object::Pad
モジュールからのフィードバックをもとにCorプロジェクトの仕様が詰められていくサイクルが繰り返されます。
その成果を受けて、ようやくPerl本体に実験的機能としてclass
キーワードと、それに付随するfield
キーワード、method
キーワードが導入されたのが2023年2月のこと。本稿執筆時点ではまだ開発版ですが、本誌が発売されるころにはリリースされているであろうPerl 5.
クラスの基本的な使い方
Perl本体でのクラスの実装は始まったばかりですので、Moose
系のモジュールやObject::Pad
モジュールに比べるとできることはごく限られています。たとえば、先ほどのPerson
モジュールをクラスで書きなおすと次のようになります。
use v5.37;
use experimental 'class';
class Person {
field $name :param;
method name { $name }
method say_hi { say "Hi, I'm $name." }
}
field
キーワードはMoose
系のモジュールにおけるhas
キーワードに相当し、クラス変数を定義します。method
キーワードはクラスメソッドを定義します。
オブジェクトの生成方法は従来どおりnew
メソッドを利用します。
use Person;
my $p = Person->new(name => 'ishigaki');
$p->say_hi; # Hi, I'm ishigaki
サブルーチンシグネチャ対応
クラスメソッドは、Perl 5.
use v5.37;
use experimental 'class';
class Person {
...
method say_hi_to ($person = "there") {
say "Hi, $person!";
}
}
クラスの継承
クラスの継承には:isa
属性を利用します。
use v5.37;
use experimental 'class';
class Author :isa(Person) {
field $pause_id :param;
method pause_id { $pause_id }
}
なお、クラスの多重継承はできません。単純な継承では実現できない機能追加には、後述するロールを利用することになるものと予想されます。
初期化時の調整
ADJUST
ブロックを利用すると、オブジェクト生成時にパラメータを受け取らない変数の初期化などを行うことができます。
なお、親クラスのfield
文で定義した変数は子クラスからは見えません。親クラスのメソッド経由で値を取り出す必要があります。
use v5.37;
use experimental 'class';
class Author :isa(Person) {
...
field $name_and_id;
ADJUST {
$name_and_id =
$self->name . " ($pause_id)";
}
method name_and_id { $name_and_id }
}
安全性を担保するしくみ
クラスには、安全なコードを書いてもらうためのしかけがいくつも用意されています。
Moose
系のモジュールを使ったモジュールの場合、has
文で登録した属性の名前をサブルーチンの中などで書き間違えても実行時まで気付けませんでした。ところが、クラスのメソッド内でクラス変数の名前を間違うと、コンパイル時にエラーが発生します。
use v5.37;
use experimental 'class';
class Person {
field $name :param;
method name { $neme } # コンパイルエラー
...
}
Moose
系のモジュールの場合、オブジェクトの内部は
use v5.37;
use experimental 'class';
class Author :isa(Person) {
ADJUST {
# 実行時エラー
$self->{pause_id} =~ tr/a-z/A-Z/;
}
...
}
sub
文で宣言した関数とは異なり、method
文で宣言したメソッドはオブジェクトからしか呼び出せません。
通常の関数のように呼び出すとエラーになります。
use Person;
Person::say_hi(); # エラー
Person->say_hi(); # エラー
<続きの