Kichijoji.pm20で発表した, 「詳解 Class::Accessor::Typed」の資料を公開します. 今回は実験的にScrapboxに書いてプレゼンテーションモードで発表したので, それを加筆修正してはてなブログで公開することとしました.
Perlの「型」について
- そもそも型とは?
- Wikipedia曰く...
コンピュータで扱うデータの決められた形式・種類(整数型、浮動小数点型、文字型、集合型、ポインタ型 等)→データ型
- Perlは動的型付け言語
- GoやScalaのような静的な型はない
- perldocのperldataによると...
Perl には、スカラ、スカラの配列、「ハッシュ」とも呼ばれるスカラの 連想配列という 3 つの組み込みデータ型があります。
- ナルホド...
型制約
- 例えば...
sub hello { my $name = shift; printf "Hello, %s!!!", $name; } hello('papix'); # => Hello, papix!!! hello(+{}); # => Hello, HASH(0x7fdabc8035e0)!!!
- Perlのスカラ変数は(当然)様々な種類のデータを持てる
- 文字列のようなもの, 数字のようなもの, 配列やハッシュのリファレンス...
- 例えば, 文字列を期待する所にリファレンスが来ると困る
- エラーになる, 警告が出る...
- 何れにせよ, 期待通りの結果にならない!!
- 「期待通りの結果にならない」のに, 正しく動作するように見えるとかなり辛い
- 例えば, 上のサンプルコードは
HASH(0x7fdabc8035e0)
という意図しない出力をするが, エラーにはならず処理はそのまま実行され続ける(警告は出る)
- 静的型付け言語であれば, コンパイル時にエラーとすることができる
- しかし, 動的型付け言語は実行時に型が動的に定まるので, コンパイル時にエラーにすることができない...
ソリューション
- モジュールで動的に型を定義するアプローチ
- 前述のように, Perlは動的型付け言語 = 実行するまで何が来るかわからない
- モジュールで型を定義し, 実行時にそれを使って検証する
- 型が適切でない場合, エラーになる
- しかし, エラーにならず, そのまま素通りしてしまうよりは遥かにマシ
- モジュールで型を定義し, 実行時にそれを使って検証する
- 例: Smart::Args
hello(+{});
は, Smart::Argsによってエラーとなる(その時点でdieする)
- 前述のように, Perlは動的型付け言語 = 実行するまで何が来るかわからない
use Smart::Args qw(args_pos); sub hello { args_pos my $name => 'Str'; printf "Hello, %s!!!", $name; } hello('papix'); # => Hello, papix!!! hello(+{}); # => '$name': Validation failed for 'Str' with value HASH(0x7f8f298035e0)
- 型の定義
- 例: Mouse::Util::TypeConstraints
- クラスビルダーのMouseに付属している, 基本的な型 + 型の拡張を実現するモジュール
- Smart::Argsも内部ではこれを使っている
- Type::Tinyというモジュールを使ったSmart::Args::Type::Tinyというモジュールもある
- 次のような型が用意されている:
- クラスビルダーのMouseに付属している, 基本的な型 + 型の拡張を実現するモジュール
- 例: Mouse::Util::TypeConstraints
Any Item Bool Maybe[] Undef Defined Value Str Num Int ClassName RoleName Ref ScalarRef ArrayRef[] HashRef[] CodeRef RegexpRef GlobRef FileHandle Object
Perlのクラスビルダーについて
クラスビルダーとは...
- Perlには, もともとオブジェクト指向プログラミングという概念はなかった
- 後に,
bless
などの仕組みを作って, 「後付で」実装された
- 後に,
package MyPackage; sub new { my ($class, $name) = @_; return bless { # blessを使って, hash referenceをMyPackageで"祝福"する name => $name, }, $class; } sub name { my $self = shift; return $self->{name}; }
use MyPackage; my $obj = MyPackage->new(name => 'papix'); print $obj->name; # => papix
- このあたりの記述を, より便利に/シンプルに記述できるようにするのがPerlにおける「クラスビルダー」
- Perl界では, さっくり大別して, 「フルスペック」なクラスビルダーと「シンプル」なクラスビルダーがある
- フルスペック
- getter/setter(アクセサ)などが自動的に提供される
- 型によるバリデーションなどがある
- Mixinなどを実現するための仕組みがある
- シンプル
- getter/setter(アクセサ)の提供をするくらい
- フルスペック
フルスペック
- Mouse
- XSを活用しまくっていて軽量
- 国内ではデファクトスタンダード(と思う)
- 注: XSというのは, Perlの処理をC言語で実装するための仕組みです
- C言語で実装されているのでPerlに比べて高速
- XSを活用しまくっていて軽量
- Moose / Moo
- 最近はMooを使うよう推奨されている
- Moose / Mooをよしなに使い分ける, Any::Mooseというモジュールもあった
- Mo, Mとかもある... (ネタモジュールの域)
シンプル
- Class::Accessor / Class::Accessor::Fast
- Class::Accessor::Lite
- Class::Accessorを更にシンプルにした実装
- Class::Accessor::Lite::Lazy
- Class::Accessor::Liteに遅延ロードを提供するモジュール
Class::Accessor::Lite
new
が真ならコンストラクタを自動生成するrw
/ro
/wo
で, それぞれread/write, read only, write onlyのアクセサを生成する
package MyPackage; use Class::Accessor::Lite ( new => 1, rw => [ qw(foo) ], ro => [ qw(bar) ], );
use MyPackage; my $obj = MyPackage->new( foo => 'a', bar => 'b', ); print $obj->foo; # => a $obj->foo(123); print $obj->foo; # => 123 print $obj->bar; # => b $obj->bar(123); # 'main' cannot access the value of 'bar' on objects of class 'MyPackage'
Class::Accessor::Lite::Lazy
- Class::Accessor::Liteに加えて,
rw_lazy
,ro_lazy
を提供- コンストラクタに値を渡さなかった時, デフォルト値を遅延して生成できる(アクセサを読みに行った時に生成する)
- 例えば次のコードは,
foo
を読み込んだ時に_build_foo
の返り値が自動的に初期値として使われる
package MyPackage; use Class::Accessor::Lite::Lazy ( rw_lazy => { foo => '_build_foo', } ); sub _build_foo { my $self = shift; ... }
課題感
- フルスペックのクラスビルダーは重厚長大
- コードも複雑
- しかし型の恩恵を得られるのは利点
- シンプルなクラスビルダーはコードも単純
- しかし型の恩恵はない
Class::Accessor::Typedの紹介
- シンプルなクラスビルダー(Class::Accessor::Lite)を軸に, 型の恩恵を受けられるようにした
- Smart::Args like (Mouse::Util::TypeConstraintsベース)
- Class::Accessor::Lite / Class::Accessor::Lite::Lazy + Smart::Args, みたいな雰囲気
- はてなではよく使われる組み合わせ
- シンプルなクラスビルダーのようにコードは(フルスペックのクラスビルダーと比べると)かなり単純で, しかし型の恩恵を受けることができる
使い方
Class::Accessor::LiteやClass::Accessor::Lite::Lazyを使ったことがある方であれば, 使い方がかなり似ているということがわかるのではないかと思います.
package MyPackage; use Class::Accessor::Typed ( rw => { baz => { isa => 'Str', default => 'string' }, }, ro => { foo => 'Str', bar => 'Int', }, wo => { hoge => 'Int', }, rw_lazy => { foo_lazy => 'Str', } ro_lazy => { bar_lazy => { isa => 'Int', builder => 'bar_lazy_builder' }, } new => 1, ); sub _build_foo_lazy { 'string' } sub bar_lazy_builder { 'string' }
以下, Class::Accessor::Typedでできる事をコードを例示しつつ説明します.
型のチェック
コンストラクタに渡した値が指定した型を満たさない場合, エラーになります.
{ package MyPackage; use Class::Accessor::Typed ( rw => { foo => 'Int', }, new => 1, ); } my $obj = MyPackage->new( foo => 'string' ); # => 'foo': Validation failed for 'Int' with value string
もちろん, setterで値を上書きした時も型によるチェックが行われます.
{ package MyPackage; use Class::Accessor::Typed ( rw => { foo => 'Int', }, new => 1, ); } my $obj = MyPackage->new( foo => 123 ); $obj->foo('string'); # => 'foo': Validation failed for 'Int' with value string
コンストラクタ
コンストラクタの引数が足りない場合, エラーになります.
{ package MyPackage; use Class::Accessor::Typed ( rw => { foo => 'Int', }, new => 1, ); } my $obj = MyPackage->new(); # => missing mandatory parameter named '$foo'
ちなみに存在しないパラメータを渡した時は警告が出ます.
{ package MyPackage; use Class::Accessor::Typed ( rw => { foo => 'Int', }, new => 1, ); } my $obj = MyPackage->new(foo => 1, bar => 1); # unknown arguments: bar
コンストラクタの引数が足りない時, もしdefault
オプションが指定されていれば, それを利用します.
{ package MyPackage; use Class::Accessor::Typed ( rw => { foo => { isa => 'Int', default => 123 }, }, new => 1, ); } my $obj = MyPackage->new(); print $obj->foo; # => 123
また, rw_lazy
, ro_lazy
を利用すれば, アクセサから読み込んだ時に値を生成できます.
{ package MyPackage; use Class::Accessor::Typed ( rw_lazy => { foo => 'Int', }, new => 1, ); sub _build_foo { 123 } } my $obj = MyPackage->new(); print $obj->foo; # => 123
ベンチマーク
Rate Moose Moo(ISA) C::A::Typed Moo C::A::Fast Mouse C::A::Lite Moose 25830/s -- -89% -92% -96% -96% -97% -97% Moo(ISA) 240604/s 831% -- -27% -60% -62% -70% -76% C::A::Typed 328975/s 1174% 37% -- -45% -49% -59% -67% Moo 595781/s 2207% 148% 81% -- -7% -25% -39% C::A::Fast 641222/s 2382% 167% 95% 8% -- -20% -35% Mouse 799455/s 2995% 232% 143% 34% 25% -- -19% C::A::Lite 983039/s 3706% 309% 199% 65% 53% 23% --
- Mooseや, 型のチェックをするMoo(
Moo(ISA)
)に比べると, Class::Accessor::Typedは高速 - 流石に, Class::Accessor::Fast, Class::Accessor::Liteよりは遅い
- 驚異的なのはMouseで, なんとフルスタックなクラスビルダーなのにClass::Accessor::Typedより普通に高速(!?)
- 流石XS...!!!
展望
- Anikiの
inflate
のような仕組みを入れたい- 初期値を渡した時, 指定したサブルーチンを通して格納する
- 例えば, 日付の文字列を渡すと, サブルーチンを通してDateTimeなどに自動的に変換するとか...
- 以下のコードでは
filter
というオプションにしているけれど, どういう名前が良いか...?- ご意見募集中です...
package MyPackage; use Class::Accessor::Typed ( rw => { name => { isa => 'Str', filter => sub { $_[0] . "!!!" } }, }, );
use MyPackage; my $obj = MyPackage->new(name => 'papix'); print $obj->name; # => papix!!!
※Kichijoji.pm20終了後, ひとまず実装してみました:
まとめ
Class::Accessor::Typedを作ってみました. フルスタックではないけれど, そこそこ可読性が高く便利で, かゆい所に手が届くモジュールになったのではないかと思っています. 興味がある方は是非使ってみて下さい. また, ご意見ご感想やPull Requestも常時募集中です.