Masteries

技術的なことや仕事に関することを書いていきます.

詳解 Class::Accessor::Typed

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する)
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というモジュールもある
      • 次のような型が用意されている:
 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に比べて高速
  • 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終了後, ひとまず実装してみました:

github.com

まとめ

Class::Accessor::Typedを作ってみました. フルスタックではないけれど, そこそこ可読性が高く便利で, かゆい所に手が届くモジュールになったのではないかと思っています. 興味がある方は是非使ってみて下さい. また, ご意見ご感想やPull Requestも常時募集中です.