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

Perl の新 class 構文を使ってみる

Perl 5 のオブジェクト指向機能は基本的には Python の影響を受けたものだが、データを名前空間 (package) に bless する機構だけで Perl 4 以来の名前空間とサブルーチンをそのままクラスとメソッドに転換し第一級のオブジェクト指向システムとした言語設計は驚嘆に価する。 実際この言語のオブジェクトシステムは動的型付言語のオブジェクト指向プログラミングに要求されるおよそあらゆる機能を暗にサポートしており、CPAN には Moose を筆頭とした屋下屋オブジェクトシステムが複数存在しているがその多くは Pure Perl ライブラリである。つまり「やろうと思えば全部手書きで実現できる」わけである。

そういうわけで Perl のオブジェクト指向プログラミングサポートは機能面では (静的型検査の不在という現代的には極めて重大な欠如を除けば) 申し分ないのだが、しかし Moose その他の存在が示しているように一つ明らかな欠点がある。記述の冗長さだ。 コンストラクタを含むあらゆるメソッドは第一引数としてレシーバを受ける単なるサブルーチンとして明示的に書く必要があるし、オブジェクトのインスタンス変数 (a.k.a. プロパティ / データメンバ) は bless されたデータに直接的ないし間接的にプログラマ定義の方法で格納されるためアクセス手段は実装依存である。これはカプセル化の観点からは望ましい性質だが、他者の書いたクラスを継承するときに問題となる。ある日データ表現を変更した親クラスがリリースされると突然自分の書いた子クラスが実行時エラーを起こすようになるわけだ。 そうならないためにはインスタンス変数へのアクセスに (protected な) アクセサを使う必要があるのだが、そのためには親クラスが明示的にそれらを提供している必要があるし、そもそも Perl にはメソッドのアクセス修飾子というものがないので完全な制御を与えるならばオブジェクトの内部状態がすべて public になってしまう。

そのような事情もあり、特にパフォーマンスが問題にならないようなアプリケーションコードでは Moose のようなリッチな語彙を提供するオブジェクトシステムを使うことが公式のチュートリアルでも推奨されてきた。Perl コアのオブジェクトシステムの改良は近年まで極めて限定的だった。

何やら風向きが変わったのは 2023 年リリースの Perl 5.38 からで、このバージョンから実験的機能として新たな class 構文が提供されている。特筆すべきなのはこれが bless ベースの単なる構文糖衣ではなく、C レベルで新たな API を追加したオブジェクトシステムの拡張という点である。

実際オブジェクトを印字してみると “Foo=OBJECT(0xdeadbeef)” などと表示されるので、クラス Foo に結び付いているのがハッシュや配列などではなく Perl スクリプトレベルからは不透明な OBJECT というデータ構造であることが分かる。 先述したインスタンス変数問題についても組み込みの field 宣言が提供されており、オブジェクトの内部データ表現について言語レベルでの「標準的な」方法が初めて提案されたことになる。

使い方

class 構文は Perl 5.40 時点では実験的機能であり feature プラグマで明示的に有効化する必要がある。また有効化したところで実験的機能としての警告は出力されるので、これが鬱陶しければ警告を明示的に無効化することも必要である。警告クラスは experimental::class である。

注意すべき点としてこれらのプラグマタによる宣言は class キーワードが出現するに行う必要がある (さもなくば class は関数名として解釈されるだろう) 上に、class 宣言のでも後述する field / method キーワードのためにあらためて記述する必要がある:

# In package |main| here.
use v5.40;                            # Enables |strict|, |warnings|, subroutine signatures and so on.
use feature qw/class/;                # For |class| keyword.
no warnings qw/experimental::class/;  # Suppress warning message related to class feature.

class Foo {
  use v5.40;
  use feature qw/class/;                # For |field| / |method| keywords.
  no warnings qw/experimental::class/;

  field $attr :param;
  
  method do_something() { ... }
}

class

class キーワードは大雑把に言っておまけ付きの package である。Raku (former Perl 6) とまるで同じなので知っている人はこの節は飛ばして良い。

名前を必須として、バージョンとブロックを任意に伴って宣言できる。 ブロックを持つ場合はクラス定義のスコープはそのブロック内、持たない場合は出現場所以降のファイルスコープになる。なお Perl 5.40.0 においてブロックを伴わない宣言は :isa アトリビュートによる継承を行った場合に SEGV が発生する不具合が知られているためブロックを持つ宣言の方が無難である。 バージョンを指定した場合宣言したクラス (名前空間でもある) の $VERSION 変数にセットされる。これはクラスメソッド VERSION としても参照可能なのは衆知のとおり。

class 宣言をするとコンストラクタ new が自動的に定義される。既存の Perl OO においてコンストラクタは単に慣習的な名前を持つクラスメソッドだが、新構文において他の名前で唯一のコンストラクタを定義する方法はない。

class Foo 0.01 {
  use v5.40;
  use feature qw/class/;
  no warnings qw/experimental::class/;
}

my $obj = Foo->new;
say Foo->VERSION;  # "0.01".

method

method キーワードは特殊なサブルーチン宣言 (sub) である。シグネチャにレシーバ ($self) の宣言が必要なく (なお sub に対するシグネチャが no feature qw/signatures/ で無効になっている場合でも常にシグネチャが有効である)、後述の field で宣言されたインスタンス変数にも暗黙にアクセス可能になるという点が特別だが、それ以外の面では sub で定義されたサブルーチンと同等である。無名メソッドも作れるしそれを実行時に名前空間に束縛することもできる:

class Foo {
  use v5.40;
  use feature qw/class/;
  use Symbol qw//;
  no warnings qw/experimental::class/;

  method create_printer($name, $message = 'hello, world') {
    # Creates an anonymous method.
    my $meth = method () { say $message, "; from $self"; };
    # If called with |$name|, bind the method to a symbol table entry so it can
    # be called with name later.
    if (defined $name) {
      my $gref = Symbol::qualify_to_ref($name, __CLASS__);
      $gref->** = $meth;
    }
    return $meth;
  }
}

my $obj = Foo->new;

$obj->create_printer('hello');
$obj->create_printer('hello_again', 'hello (again)');
$obj->hello;        # 'hello, world; from Foo=OBJECT(0xdeadbeef)'
$obj->hello_again;  # 'hello (again); from Foo=OBJECT(0xdeafbeef)'

my $meth = $obj->create_printer(undef, 'anonymouse method!');
$obj->$meth;  # 'anonymouse method!; from Foo=OBJECT(0xdeafbeef)'

field

空のオブジェクトを作れてもあまり嬉しくはないのでインスタンス変数を持てる必要がある。それを宣言するためのキーワードが field である。my / our などと同じ変数修飾子で、宣言された変数は同クラスのメソッド内でのみ参照可能であり、インスタンス毎に異なる値を保持する。なおクラス変数は可視性に応じて my / our で宣言すれば良い。

field 変数にはいくつか専用のアトリビュートを付加することができる。 :param はスカラ変数のみに指定でき、その値をキーワード引数として受け取るようにコンストラクタ new が生成される。 :reader を指定すると変数名と同名の読み取り専用アクセサが生成される。 また開発版の Perl 5.41.7 で :writer も追加されており、こちらは Perl Best Practices 流に set_ という接頭辞を付加した名前の書き込み専用アクセサを定義する。 なお別名を指定したい場合はアトリビュートの引数として指定できる: :reader(alias_accessor_name)

class Foo {
  use v5.40;
  use feature qw/class/;
  no warnings qw/experimental::class/;
  
  # Mandatory private parameter.
  field $attr :param;

  # Optional readonly parameter.
  field $ro_attr :reader :param //= 42;
  
  method show_attr() {
    say $attr;
  }
}

my $obj = Foo->new(attr => 'this is required');
say $obj->ro_attr;  # '42'
$obj->show_attr;    # 'this is required'

ADJUST ブロック

先述のようにコンストラクタ new は自動的に定義されるので、その中にプログラマ定義の初期化コードが置けない。 これではオブジェクト生成時に値のバリデーションとかロギングとかその他を行えず不都合なので、クラス内に ADJUST という名前のブロック (いわゆるフェイザー; Phaser) を置くことでオブジェクト生成中に任意のコードを実行することができるようになっている。

このブロックは $self にアクセスでき、要するに Moose / Moo の BUILD メソッドのようなものだが、BUILD がオブジェクトの初期化が完了した後 (つまりすべてのインスタンス変数が初期化された後) に呼ばれるのに対して、ADJUSTfield 変数の初期化と同じタイミングで実行される点が異なる。つまり field 変数の値にアクセスしたい場合はその宣言の後に置かなければならない。複数個定義することもでき、その場合定義順に実行される:

class Foo {
  use v5.40;
  use feature qw/class/;
  use Carp qw//;
  use List::Util qw//;
  no warnings qw/experimental::class/;

  # Private class variable.
  my $last_id = 0;

  field $id :reader = ++$last_id;
  
  ADJUST {
    say "@{[__CLASS__]} with ID = $id is under construction...";
  }

  field $args :param :reader;

  ADJUST {
    Carp::croak 'Empty |args| is not allowed.' if $args->@* == 0;
  }

  method sum_args(@others) {
    List::Util::sum $args->@ @others;
  }
}

継承

クラスの単一継承のために :isa アトリビュートが提供されている。この構文による多重継承は現在サポートされていないが、use parent などで継承することを妨げるものはない。

継承すると親クラスのメソッドは子クラスから参照可能になるが、field で宣言したインスタンス変数は参照できない。必要ならば :reader などでアクセサを用意する必要がある。

class FooPlusOne :isa(Foo) {
  use v5.40;
  use feature qw/class/;
  no warnings qw/experimental::class/;

  method sum_args(@others) {
    $self->SUPER::sum_args(@others) + 1;
  }
}

感想

これまでプログラマの裁量に任されていたコンストラクタやレシーバの名称がユーザ定義コードと衝突し得る形で暗黙に設定されるなどあまり Perl らしくない機能ではある。 もっとも近年の新機能は言語要素の直交性より現代的な Perl プログラマの慣習・予断を重視して意味論が設計されているきらいがある (e.g., サブルーチンや catch ブロックのシグネチャが暗黙にレキシカル変数になる) ので、その流れに沿ったものであると言えるのかも知れない。

静的解析が容易な宣言的構文が導入されることにより、LSP サーバなどが対応してくれば開発環境の改善に期待が持てるだろう。 しかしながら現時点ではあまりにも機能が限定的であり記述量も Moose などと変わらず (ついでに Moose には MOP もあるので「静的」を諦めれば解析もできる)、既存のモジュールが現在使用しているオブジェクトシステムから移行する動機は大きく欠いている。

実験的機能の段階であり現に開発版 Perl で機能追加が続いている状況で結論を下すのはあまりにも早計に違いないが、巨大な CPAN 資産を抱えるエコシステムがこの新構文を受け入れてその利を得るまでには長い時間が必要だろう。

コメント

このブログの人気の投稿

BuckleScript が ReScript に改称し独自言語を導入した

Via: BuckleScript Good and Bad News - Psellos OCaml / ReasonML 文法と標準ライブラリを採用した JavaScript トランスパイラである BuckleScript が ReScript に改称した。 公式サイトによると改称の理由は、 Unifying the tools in one coherent platform and core team allows us to build features that wouldn’t be possible in the original BuckleScript + Reason setup. (単一のプラットフォームとコアチームにツールを統合することで従来の BuckleScript + Reason 体制では不可能であった機能開発が可能になる) とのこと。要は Facebook が主導する外部プロジェクトである ReasonML に依存せずに開発を進めていくためにフォークするという話で、Chromium のレンダリングエンジンが Apple の WebKit から Google 主導の Blink に切り替わったのと似た動機である (プログラミング言語の分野でも Object Pascal が Pascal を逸脱して Delphi Language になったとか PLT Scheme (の第一言語) が RnRS とは別路線に舵を切って Racket になったとか、割とよくある話である。) 公式ブログの Q&A によると OCaml / ReasonML 文法のサポートは継続され、既存の BuckleScript プロジェクトは問題なくビルドできるとのこと。ただし現時点で公式ドキュメントは ReScript 文法のみに言及しているなど、サポート水準のティアを分けて ReScript 文法を優遇することで移行を推進していく方針である。 上流である OCaml の更新は取り込み、AST の互換性も維持される。将来 ReScript から言語機能が削除されることは有り得るが、OCaml / ReasonML からは今日の BuckleScript が提供する機能すべてにアクセスできる。 現時点における ReScript の ...

Perl のサブルーチンシグネチャ早見表

Perl のサブルーチン引数といえば実引数への参照を保持する特殊配列 @_ を手続き的に分解するのが長らくの伝統だった。これはシェルの特殊変数 $@ に由来する意味論で、おそらく JavaScript の arguments 変数にも影響を与えている。 すべての Perl サブルーチンはプロトタイプ宣言がない限りリスト演算子なので、この流儀は一種合理的でもあるのだが、実用的にそれで良いかというとまったくそうではないという問題があった; 結局大多数のサブルーチンは定数個の引数を取るので、それを参照する形式的パラメータが宣言できる方が都合が良いのである。 そういうわけで実験的に導入されたサブルーチンシグネチャ機能により形式的パラメータが宣言できるようになったのは Perl 5.20 からである。その後 Perl 5.28 において出現位置がサブルーチン属性の後に移動したことを除けば Perl 5.34 リリース前夜の今まで基本的に変わっておらず、未だに実験的機能のままである。 おまじない シグネチャは前方互換性を持たない (構文的にプロトタイプと衝突している) 実験的機能なのでデフォルトでは無効になっている。 そのため明示的にプラグマで利用を宣言しなければならない: use feature qw/signatures/; no warnings qw/experimental::signatures/; どの途みんな say 関数のために使うので feature プラグマは問題ないだろう。実験的機能を断りなしに使うと怒られるので、 no warnings で確信犯であることをアピールする必要がある。 これでプラグマのスコープにおいてサブルーチンシグネチャ (と :prototype 属性; 後述) が利用可能になり、 従来のプロトタイプ構文が無効になる。 使い方 対訳を載せておく。シグネチャの方は実行時に引数チェックを行うので厳密には等価でないことに注意: # Old School use feature qw/signatures/ 1 sub f { my ($x) = @_; ... } sub f($x) { ... } 2 sub f { my ($x, undef, $y) = @_...

(multi-)term-mode に dirtrack させる zsh の設定

TL;DR .zshrc に以下を書けば良い: # Enable dirtrack on(multi-)term-mode. if [[ " $TERM " = eterm * ]]; then chpwd() { printf '\032/%s\n' " $PWD " } fi 追記 (May 14, 2025): oh-my-zsh を使っていれば emacs プラグインが勝手にやってくれる: plugins = ( emacs ) 仔細 term-mode は Emacs 本体に付属する端末エミュレータである。基本的には Emacs 内でシェルを起動するために使うもので、古い shell-mode よりも端末に近い動きをするので便利なのだが、一つ問題がある。シェル内でディレクトリを移動しても Emacs バッファの PWD がそのままでは追従しない点だ。 こういう追従を Emacs では Directory Tracking (dirtrack) と呼んだりするが、 shell-mode や eshell ではデフォルトで提供しているのに term-mode だけそうではない。 要するにシェル内で cd してもバッファの PWD は開いた時点のもの (基本的には直前にアクティヴだったバッファの PWD を継承する) のままなので、移動したつもりで C-x C-f などをするとパスが違ってアレっとなることになる。 実は term-mode にも dirtrack 機能自体は存在しているのだが、これは シェルがディレクトリ移動を伴うコマンドを実行したときに特定のエスケープシーケンスを含んだ行を印字することで Emacs 側に通知するという仕組み になっている。 Emacs と同じく GNU プロジェクトの成果物である bash は Emacs 内での動作を検出すると自動的にこのような挙動を取るが、zsh は Emacs の事情なんか知ったことではないので手動で設定する必要がある。 まずもって「ディレクトリ移動のコマンドをフックする」必要がある訳だが、zsh の場合これは簡単で cd / pushd / popd のようなディレクトリ移...

Perl 7 より先に Perl 5.34 が出るぞという話

Perl 5 の次期バージョンとして一部後方互換でない変更 (主に間接オブジェクト記法の削除とベストプラクティスのデフォルトでの有効化) を含んだメジャーバージョンアップである Perl 7 がアナウンスされたのは昨年の 6 月 のことだったが、その前に Perl 5 の次期周期リリースである Perl 5.34 が 5 月にリリース予定 である。 現在開発版は Perl 5.33.8 がリリースされておりユーザから見える変更は凍結、4 月下旬の 5.33.9 で全コードが凍結され 5 月下旬に 5.34.0 としてリリース予定とのこと。 そういうわけで事前に新機能の予習をしておく。 8進数数値リテラルの新構文 見た瞬間「マジかよ」と口に出た。これまで Perl はプレフィクス 0 がついた数値リテラルを8進数と見做してきたが、プレフィクスに 0o (zero, small o) も使えるようになる。 もちろんこれは2進数リテラルの 0b や 16進数リテラルの 0x との一貫性のためである。リテラルと同じ解釈で文字列を数値に変換する組み込み関数 oct も` 新構文を解するようになる。 昨今無数の言語に取り入れられているリテラル記法ではあるが、この記法の問題は o (small o) と 0 (zero) の区別が難しいことで、より悪いことに大文字も合法である: 0O755 Try / Catch 構文 Perl 5 のリリース以来 30 年ほど待たれた実験的「新機能」である。 Perl 5 における例外処理が特別な構文でなかったのは予約語を増やさない配慮だったはずだが、TryCatch とか Try::Tiny のようなモジュールが氾濫して当初の意図が無意味になったというのもあるかも知れない。 use feature qw/ try / ; no warnings qw/ experimental::try / ; try { failable_operation(); } catch ( $e ) { recover_from_error( $e ); } Raku (former Perl 6) だと CATCH (大文字なことに注意) ブロックが自分の宣言されたスコープ内で投げられた例外を捕らえる...