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

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 資産を抱えるエコシステムがこの新構文を受け入れてその利を得るまでには長い時間が必要だろう。

コメント

このブログの人気の投稿

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 の良いところは (ハイパー)...

(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 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 を埋めます。 代入ではなくバインディングを使っていることに注意して下さい: 配列への代入は先行評価を強制することがあります(コンパイラがリストの無限性に気づかない限り; 無限リスト検出の詳細はまだ固まっていません)。 バインディングはそのようなことがありません。 遅延性は無限リストの取り扱いを可能にします: 引数すべてに操作を行うようなことさえしなければ、評価された要素に必要なだけのメモリしか必要としません。 しかし落とし穴があります: 長さの...