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(+{});
- Perlのスカラ変数は(当然)様々な種類のデータを持てる
- 文字列のようなもの, 数字のようなもの, 配列やハッシュのリファレンス...
- 例えば, 文字列を期待する所にリファレンスが来ると困る
- エラーになる, 警告が出る...
- 何れにせよ, 期待通りの結果にならない!!
- 「期待通りの結果にならない」のに, 正しく動作するように見えるとかなり辛い
- 例えば, 上のサンプルコードは
HASH(0x7fdabc8035e0)
という意図しない出力をするが, エラーにはならず処理はそのまま実行され続ける(警告は出る)
- 静的型付け言語であれば, コンパイル時にエラーとすることができる
- しかし, 動的型付け言語は実行時に型が動的に定まるので, コンパイル時にエラーにすることができない...
ソリューション
- モジュールで動的に型を定義するアプローチ
- 前述のように, Perlは動的型付け言語 = 実行するまで何が来るかわからない
- モジュールで型を定義し, 実行時にそれを使って検証する
- 型が適切でない場合, エラーになる
- しかし, エラーにならず, そのまま素通りしてしまうよりは遥かにマシ
- 例: Smart::Args
hello(+{});
は, Smart::Argsによってエラーとなる(その時点でdieする)
use Smart::Args qw(args_pos);
sub hello {
args_pos my $name => 'Str';
printf "Hello, %s!!!", $name;
}
hello('papix');
hello(+{});
- 型の定義
- 例: Mouse::Util::TypeConstraints
- クラスビルダーのMouseに付属している, 基本的な型 + 型の拡張を実現するモジュール
- Smart::Argsも内部ではこれを使っている
- Type::Tinyというモジュールを使ったSmart::Args::Type::Tinyというモジュールもある
- 次のような型が用意されている:
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 {
name => $name,
}, $class;
}
sub name {
my $self = shift;
return $self->{name};
}
use MyPackage;
my $obj = MyPackage->new(name => 'papix');
print $obj->name;
- このあたりの記述を, より便利に/シンプルに記述できるようにするのがPerlにおける「クラスビルダー」
- Perl界では, さっくり大別して, 「フルスペック」なクラスビルダーと「シンプル」なクラスビルダーがある
- フルスペック
- getter/setter(アクセサ)などが自動的に提供される
- 型によるバリデーションなどがある
- Mixinなどを実現するための仕組みがある
- シンプル
- getter/setter(アクセサ)の提供をするくらい
フルスペック
- Mouse
- XSを活用しまくっていて軽量
- 注: XSというのは, Perlの処理をC言語で実装するための仕組みです
- 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;
$obj->foo(123);
print $obj->foo;
print $obj->bar;
$obj->bar(123);
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' );
もちろん, setterで値を上書きした時も型によるチェックが行われます.
{
package MyPackage;
use Class::Accessor::Typed (
rw => {
foo => 'Int',
},
new => 1,
);
}
my $obj = MyPackage->new( foo => 123 );
$obj->foo('string');
コンストラクタ
コンストラクタの引数が足りない場合, エラーになります.
{
package MyPackage;
use Class::Accessor::Typed (
rw => {
foo => 'Int',
},
new => 1,
);
}
my $obj = MyPackage->new();
ちなみに存在しないパラメータを渡した時は警告が出ます.
{
package MyPackage;
use Class::Accessor::Typed (
rw => {
foo => 'Int',
},
new => 1,
);
}
my $obj = MyPackage->new(foo => 1, bar => 1);
コンストラクタの引数が足りない時, もしdefault
オプションが指定されていれば, それを利用します.
{
package MyPackage;
use Class::Accessor::Typed (
rw => {
foo => { isa => 'Int', default => 123 },
},
new => 1,
);
}
my $obj = MyPackage->new();
print $obj->foo;
また, 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;
ベンチマーク
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より普通に高速(!?)
展望
- 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;
※Kichijoji.pm20終了後, ひとまず実装してみました:
github.com
まとめ
Class::Accessor::Typedを作ってみました. フルスタックではないけれど, そこそこ可読性が高く便利で, かゆい所に手が届くモジュールになったのではないかと思っています.
興味がある方は是非使ってみて下さい. また, ご意見ご感想やPull Requestも常時募集中です.