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/;
$attr :param;
field
method do_something() { ... } }
class
class
キーワードは大雑把に言っておまけ付きの
package
である。Raku (former Perl 6)
とまるで同じなので知っている人はこの節は飛ばして良い。
名前を必須として、バージョンとブロックを任意に伴って宣言できる。
ブロックを持つ場合はクラス定義のスコープはそのブロック内、持たない場合は出現場所以降のファイルスコープになる。なお
Perl 5.40.0 においてブロックを伴わない宣言は :isa
アトリビュートによる継承を行った場合に SEGV が発生する不具合が知られているためブロックを持つ宣言の方が無難である。
バージョンを指定した場合宣言したクラス (名前空間でもある) の
$VERSION
変数にセットされる。これはクラスメソッド
VERSION
としても参照可能なのは衆知のとおり。
class
宣言をするとコンストラクタ new
が自動的に定義される。既存の Perl OO
においてコンストラクタは単に慣習的な名前を持つクラスメソッドだが、新構文において他の名前で唯一のコンストラクタを定義する方法はない。
0.01 {
class Foo 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/;
$name, $message = 'hello, world') {
method create_printer(# 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.
$attr :param;
field
# Optional readonly parameter.
$ro_attr :reader :param //= 42;
field
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
がオブジェクトの初期化が完了した後
(つまりすべてのインスタンス変数が初期化された後)
に呼ばれるのに対して、ADJUST
はfield
変数の初期化と同じタイミングで実行される点が異なる。つまり
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;
$id :reader = ++$last_id;
field
ADJUST {say "@{[__CLASS__]} with ID = $id is under construction...";
}
$args :param :reader;
field
ADJUST {Carp::croak 'Empty |args| is not allowed.' if $args->@* == 0;
}
@others) {
method sum_args(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/;
@others) {
method sum_args($self->SUPER::sum_args(@others) + 1;
} }
感想
これまでプログラマの裁量に任されていたコンストラクタやレシーバの名称がユーザ定義コードと衝突し得る形で暗黙に設定されるなどあまり
Perl らしくない機能ではある。
もっとも近年の新機能は言語要素の直交性より現代的な Perl
プログラマの慣習・予断を重視して意味論が設計されているきらいがある
(e.g., サブルーチンや catch
ブロックのシグネチャが暗黙にレキシカル変数になる)
ので、その流れに沿ったものであると言えるのかも知れない。
静的解析が容易な宣言的構文が導入されることにより、LSP サーバなどが対応してくれば開発環境の改善に期待が持てるだろう。 しかしながら現時点ではあまりにも機能が限定的であり記述量も Moose などと変わらず (ついでに Moose には MOP もあるので「静的」を諦めれば解析もできる)、既存のモジュールが現在使用しているオブジェクトシステムから移行する動機は大きく欠いている。
実験的機能の段階であり現に開発版 Perl で機能追加が続いている状況で結論を下すのはあまりにも早計に違いないが、巨大な CPAN 資産を抱えるエコシステムがこの新構文を受け入れてその利を得るまでには長い時間が必要だろう。
コメント
コメントを投稿