Perl Hackers Hub

第79回最近Perlに追加された実験的機能
try文⁠defer文⁠class文(1)

本連載では第一線のPerlハッカーが回替わりで執筆していきます。今回のハッカーはcharsbarこと石垣憲一さんで、テーマは「最近Perlに追加された実験的機能」です。

もとになるモジュールが存在している新機能

最近Perlに追加された新機能の中には、開発サイクルを速く回すためにCPANモジュールとしてプロトタイピングを行ったうえで、Perl本体に実験的な機能として移植し、正式な機能への昇格を待っているものがいくつかあります。本稿ではそのような実験的機能を紹介していきます。

なお、本稿のサンプルコードは基本的に執筆時の最新開発版であるPerl 5.37.11(2023年5月)で動作確認をしています。

trycatch文 ─⁠─正しいエラー処理

Perlで例外を捕捉するには通常eval文と$@特殊変数を利用しますが、これらは気を付けて使わないと、⁠特に古いPerlでは)落とし穴にはまることがあります。たとえば、次のコードには典型的な穴があります。

eval {
    ...
};
if ($@) {
    ...
}

これでもたいていは問題なく動作するのですが、$@特殊変数はさまざまな事情で空になるので、無条件に信用することはできません。安全性を重視するなら、最低限eval文の戻り値を見て成否を判定する必要があります。

eval { ...; 1; } or die "error!";

このような暗黙の了解を隠蔽いんぺいするため、2009年にはTryCatchTry::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.12では独自のキーワードを追加するプラガブルキーワードAPIが用意され、Perl 5.14(2011年)では構文解析を行うAPIも強化されました。

そのあと、これらのAPIを利用するモジュールがいくつか登場したのですが、最終的にPerl本体の実装の基礎となったのは、2016年に実験が始まったSyntax::Keyword::Tryモジュールでした。その成果を受けて、2021年にリリースされたPerl 5.34ではtryキーワードとcatchキーワードが実験的機能としてPerl本体に取り込まれ、翌2022年のPerl 5.36では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.10以降、新しいキーワードを伴う新機能については後方互換性維持のため何らかの方法でキーワードの利用を宣言する必要があります。実験的機能の場合はexperimentalプラグマを使うことで、余計な警告を出すことなく実験的なキーワードを使えます。

defer文 ─⁠─遅延実行するブロックを定義

2020年代に入ってPerlの開発体制が変わり、Syntax::Keyword::Tryモジュールの作者Paul Evans氏が開発の意思決定に積極的に関与するようになると、実験はさらに加速しました。

2021年にはSyntax::Keyword::Tryモジュールと同じくプラガブルキーワードAPIを利用したSyntax::Keyword::Deferモジュールの実験が始まり、翌年のPerl 5.36では実験的機能としてdeferキーワードが追加されました。これによって、従来は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.38で実験的機能としてお目見えするはずです。

クラスの基本的な使い方

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.36で正式な機能に昇格したサブルーチンシグネチャに対応しています。

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(); # エラー

<続きの(2)こちら。>

おすすめ記事

記事・ニュース一覧