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

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

多分週刊チラシの裏 (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...

多分週刊チラシの裏 (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 上の対話で当事者双方が納得し合っ...

Mac から iPhone のカメラを起動して写真を直接取り込める

Via: The Verge ID セルフィーや (物理) 書籍のページスキャンなど携帯電話のカメラを使って写真を取り込むことは日常的な所作になっているが、写真の使い途が何かの申し込み用 Web フォームなどで iPhone より Mac の方が操作し易いときなどは億劫だ。Mac 組込の FaceTime カメラは 720p とか 1080p しかなくて非力すぎ、かといって iPhone で一旦撮影したものを Photos から探して AirDrop するのも面倒である。 実は macOS Mojave / iOS 12 以降には Continuity Camera という機能がある。これを使うと Apple 製の Mac アプリケーションから iPhone / iPad のカメラを起動して、余計な中間コピーを残すことなく写真を Mac に転送できる。 使い方は簡単で、対応している Mac アプリケーションのコンテキストメニューに “Import (or Insert) from iPhone (or iPad)” という項目がある。“Take Photo” だと一枚、“Scan Documents” だと複数の写真を (歪み補正しつつ) 連続で撮影して転送できる。 対応 Mac アプリケーションは Finder のほか iWork (Keynote, Numbers, Pages), Mail, Messages, Notes, TextEdit となっている、のだが実は Preview でも使える。同様にコンテキストメニューあるいは “File” メニューから起動できる。

開発環境の構築に asdf が便利なので anyenv から移行した

プロジェクト毎に異なるバージョンの言語処理系やツールを管理するために、pyenv や nodenv など *env の利用はほとんど必須となっている。 これらはほとんど一貫したコマンド体系を提供しており、同じ要領で様々な環境構築ができる非常に便利なソフトウェアだが、それを使うことで別の問題が出てくる: *env 自身の管理である。 無数の *env をインストールし、シェルを設定し、場合によりプラグインを導入し、アップデートに追従するのは非常に面倒な作業だ。 幸いなことにこれをワンストップで解決してくれるソリューションとして anyenv がある。これは各種 *env のパッケージマネージャというべきもので、一度 anyenv をインストールすれば複数の *env を簡単にインストールして利用できる。さらに anyenv-update プラグインを導入すればアップデートまでコマンド一発で完了する。素晴らしい。 そういうわけでもう長いこと anyenv を使ってきた。それで十分だった。 ——のだが、 ここにもう一つ、対抗馬となるツールがある。 asdf である。anyenv に対する asdf の優位性は大きく2つある: 一貫性と多様性だ。 一貫性 “Manage multiple runtime versions with a single CLI tool” という触れ込み通り、asdf は様々な言語やツールの管理について一貫したインタフェースを提供している。対して anyenv は *env をインストールするのみで、各 *env はそれぞれ個別のインタフェースを持っている。 基本的なコマンド体系は元祖である rbenv から大きく外れないにしても、例えば jenv のように単体で処理系を導入する機能を持たないものもある。それらの差異はユーザが把握し対応する必要がある。 多様性 asdf はプラグインシステムを持っている。というより asdf 本体はインタフェースを規定するだけで、環境構築の実務はすべてプラグイン任せである。 そのプラグインの数は本稿を書いている時点でおよそ 300 を数える。これは言語処理系ばかりでなく jq などのユーティリティや MySQL のようなミドルウェアも含むが、いずれにしても膨大なツールが asdf を使えば...