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

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 ...

多分週刊チラシの裏 (Sep 28 - Oct 04, 2020)

Chrome Web Store が有料 Chrome 拡張の取扱を終了 Chrome Web Store で提供されている有料 Chrome 拡張及びアプリ内課金 API の両方が 2021 年 1 月いっぱいで廃止される。 開発者はそれまでに代替となるサードパーティの課金 API に移行し、購入済ライセンスの移行手段も用意する必要がある。 この決定の発表時点で新規の有料ないしアプリ内課金のある Chrome 拡張の新規登録は終了している。実際のところ 2020 年 3 月時点で既に「一時的に」停止されており、その措置が恒久化されただけとの由。 シェルスクリプティングには長いオプションを使え 「短いオプション (e.g., -x ) はコマンドライン上での略記である。スクリプトにおいては自分や将来の同僚のためにも長いオプション (e.g., ---do-something ) を与える方が理解が容易だろう」という主張。 異論の余地なく正論である。 CobWeb - COBOL to WebAssembly Compiler COBOL から WebAssembly へのコンパイラ。いやマジで。 Cloudflare が何を思ったか同社のサーバレス環境である Workers に COBOL 対応を追加した際 の成果物である。 COBOL から C へのトランスレータである GNU COBOL と C コードをコンパイルして WebAssembly を出力する Emscripten から成っており、他の言語に比べて軽量なバイナリを生成するとのこと。 「ウチではそんな風にはやらないんだ (“We don’t do that here”)」 昨今ソフトウェア開発のコミュニティでも Code of Conduct を用意するところが増えてきたが、コミュニティの文化を明文化するのは難しい。 長大な「べからず集」は息苦しいし、肯定的なガイドラインは時に抽象的で実効的に使えない。問題となるようなふるまいの動機が善意であった場合は特にそうだ。 仮に優れたガイドラインがあっても、それに基いて人を実際に咎めるのは骨が折れることである。初中やればコミュニティ内でも疎まれる。 話の分かる相手ならそれでもまだ説得する意義もあるが、Web 上の対話で当事者双方が納得し合っ...

多分週刊チラシの裏 (Oct 19, 2020 - Feb 26, 2021)

週刊とは言ったが毎週刊とは言ってないという言い訳。 C++ のコンパイルを高速化する小技 ビルドシステムやツールを変更せずともコーディングだけで改善できるコンパイル時間短縮テクニック。 #include を減らす インライン化を明示的に避ける 関数オーバーロードの可視性を制限する 公開シンボルを減らす の 4 本。 歯医者で歯を治したら記憶能力を失った話 歯医者で簡単な治療を受けた日から後、記憶が 90 分しか保持できなくなった英国の軍人の話。まるで「博士の愛した数式」だが実話である。 DRPK で売られていた Sim City っぽいゲームのリバースエンジニアリング 平壌市内のアプリストア (物理) で売られていた Sim City 風ゲームがインストールに失敗してライセンス認証で止まってしまったのでなんとか動かせないものかとリバースエンジニアリングしてみた話。 日本にあっては DPRK のデジタル事情というと 3G セルラーが現役とか国内 Web サイトのリストがポスター一枚に収まるとか何故かコンピュータ将棋の古豪とかの断片的な情報が伝え聞かれる程度だが、近頃は Android タブレットでゲームなどもできるらしい。 国内のインフラ及びエコシステム事情に合わせて元々フリーミアム + アプリ内課金モデルだったものが買い切り 5,000 KPW (< 1 USD) になっているなど、我々が失った自由が我々よりも不自由な (はずだと我々が信じている) 国に残存しているのは皮肉だろうか。 typosquatting は単なる typo じゃ済まない typo を狙って人気のあるドメインやソフトウェアに類似した名前をつける手法 (typosquatting) は人を辟易させるのみならずセキュリティの脅威である。 IQT が 2017 年から 2020 年にかけて Python ライブラリの中央リポジトリである PyPI において行った調査で、メジャーなライブラリに名前を似せたマルウェアが 40 個確認されたとのこと。 その内 16 個が単純なスペルミス狙い (e.g., “urlib3” vs. “urllib3”) で、26 個は正当なパッケージと混同するような名前 (e.g., “nmap-python” vs. “pytho...

多分週刊チラシの裏 (Feb 28, 2021 - Mar 22, 2021)

JavaScript 開発者が如何にして TypeScript 嫌いから TypeScript ファンになったか 気軽な読み物。型宣言の冗長さとジェネリクスなどの複雑性を嫌って (選択肢にあれば) JavaScript の方を選んできた筆者が TypeScript しか選べない職場に移って数ヶ月後にはすっかりファンになっていたという話。 理由は月並で「『不可能な状態を不可能にする』Union Type と網羅性チェック」「コンパイル時型検査によるエラーの早期検出」「リッチな IDE 支援」の 3 本。理由がそれだけなら個人的には Flow か Elm を進めたいところではある。 NASA の最新火星ローバーが搭載するプロセッサは 1998 年の iMac と同じ NASA が Mars 2020 ミッションのために送り出し、先月火星表面に着陸した最新かつ過去最大のローバーである Perseverance の話。 2021 年に活動を開始したこのハイテク・ガジェットのメインプロセッサは PowerPC 750 であるとのこと。1998 年発売の初代 iMac が搭載していた “G3” プロセッサといえば分かり易いだろう。 もちろん民生品そのものではなく、-55 - 125 ℃ の気温と 200,000 - 1,000,000 Rad の放射線に耐える特別仕様の BAE Systems RAD750 である。ちなみに「火星で自撮り」という快挙を成し遂げたのち現在も活動中の先代 Curiosity も同じものを搭載している。動作周波数 110 - 200 MHz、価格は $200,000 程度とのこと。 Internet Archive Infrastructure 過去の Web サイト、書籍、ビデオに音楽からクラシックソフトウェアまでインターネットに公開されたあらゆるデータを収集・保存する Internet Archive のインフラ紹介ビデオ。 クラウドは一切使っておらず、自前のベアメタルサーバ 750 台に接続されたストレージはシステム全体で 200PB とのこと。保存されるデータは現在のところ年 25 % 以上増大しており、四半期で 5 - 6 PB 規模だという。 Semantic Versioning はお前を救わない 「ある API ...

(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 のようなディレクトリ...