WEB+DB Press Vol.115 の Perl Hackers Hub に寄稿しました
2/22 に発売される WEB+DB Press Vol.115 の Perl Hackers Hub に「動的なモジュールロードとの付き合い方」というタイトルで寄稿させていただきました。
恐れ多くも商業誌デビューとなります。
(他記事や他の作者の方を見て、僕のような未熟な人間がこのような場にいていいのかと改めて戦慄しています・・・)
記事の内容は以下のようになっています。
- Perlで動的なモジュールロードをする方法とハマりどころ
- 動的なモジュールロードで実現できること
- 動的なモジュールロードの活用
- 動的なモジュールロードで発生する問題とその対策
過去にブログで書いた記事や、昨年11月にYAPC::Nagoya::Tiny で発表したトークを実践的に深堀りした内容ですね。
Perlではほかの言語と比べ、言語の柔軟性や文化的な要因により動的なモジュールロードが利用されることが多いと思います。
実際、拡張性や実装の手間の削減といった面で多大な恩恵もたらすので、僕も仕事や趣味でよく利用してきましたし、 CPANでも動的にロードする処理をよく見かけます。
しかし、規模が大きいプロダクトや継続的にメンテナンスされるコードでは、動的なモジュールロードが原因で保守性が低下し様々な問題が発生する・・・という経験をしてきました。
そこで、改めて動的なモジュールロードでどのようなことができるようになり、どのように活用されているかをまとめ、その上でどのような問題が起きるのか、問題に対してどのような対策をうてるのかを調査したり考えた、という内容になっています。
動的なモジュールロードはなかなか扱いが難しいので、この記事で述べたことが使いどころの指針になったり、より保守性の高いコードを書くための助けになれば幸いです。
僕の記事以外にも、競プロの過去問からアルゴリズムを学べる記事や、iOS13での新機能に関する記事、12月にリリースされたRuby2.7についての記事、さくっとゲームを作る方法を解説している記事などおもしろそうな記事がいろいろありますので、ぜひお手に取ってみてください。
よろしくお願いいたします!
今回このような貴重な機会をくださった方、記事の内容を校正してくださったりレビューしてくださった関係者の皆様方、アドバイスをくれたり応援してくださった方々にこの場を借りて御礼申し上げます。
とても嬉しかったですし、勉強になりました。
ありがとうございました。
Types::TypedCodeRef というモジュールを作りました
概要
Types::TypedCodeRef はPerlで「関数の型」をチェックする型を提供するようなモジュールです。
調べた感じ匿名サブルーチンにいい感じに引数の型とサブルーチンの返り値の型をつけてくれるようなモジュールもなさそうだったので、
AnonSub::Typed という匿名サブルーチンの引数の型と返り値の型をチェックするようなモジュールも作りました。
https://github.com/ybrliiu/p5-Types-TypedCodeRef
https://github.com/ybrliiu/p5-AnonSub-Typed
使い方
use v5.30; use Test2::V0; use Types::TypedCodeRef qw( TypedCodeRef ); use Types::Standard qw( Int Str ); use AnonSub::Typed qw( anon ); my $type = TypedCodeRef[ [Int, Int] => Int ]; ok $type->check(anon [Int, Int] => Int, sub { $_[0] + $_[1] }); ok !$type->check(0); ok !$type->check([]); ok !$type->check(sub {}); package Hoge { use Moo; use Types::Standard qw( ArrayRef Int ); use Types::TypedCodeRef qw( TypedCodeRef ); has event_handlers => ( is => 'ro', isa => ArrayRef[ TypedCodeRef[ [ Int, Int ] => Int ] ], required => 1, ); } my $hoge = Hoge->new(event_handlers => [ anon [Int, Int] => Int, sub { $_[0] + $_[1] } ]); is $hoge->event_handlers->[0]->(12, 13), 25; done_testing;
モチベーション
Perlでコード書いているときにもObserverパターン的なアーキテクチャを作りたくなることがあるのですが、
いちいち愚直にObserverパターンを実装するのは面倒くさいですし、かといってイベントハンドラみたいにCodeRefだけわたして雑に実装しようとすると、
イベントハンドラに登録するコールバック関数に渡す引数と関数の返り値の型がわかりにくくなり、コードの可読性が落ちるので、なんとかしたくなり実装しました。
最初は Function::Parameters や Function::Return あたりのモジュールで匿名サブルーチンの引数と返り値の型付けもできるかなーと思っていましたが、どうやら匿名サブルーチンの場合はattributeが有効にならないようだったので、(attributeの実装的にも恐らくそうなる)、匿名サブルーチンの引数と返り値の型チェックを行うモジュール AnonSub::Typed の実装を行ってから、Types::TypedCodeRef を実装しました。
実装について
AnonSub::Typed
型に関する情報もGCで一緒に管理されてほしいので、匿名サブルーチン本体をblessしたinside-outオブジェクトを返すクラスとして実装していて、それにパラメータの型や返り値の型情報を紐付けています。
クラスビルダーには Class::InsideOut というクラスビルダーを利用しています。
後は愚直に anon
という AnonSub::Typed
のインスタンスを作る関数を提供しています。
Types::TypedCodeRef
Type::Tiny
のコードを参考にしつつ、総称型のような型を作っています。
同じインターフェースの関数かどうかは最近kfly8さんが作られた Sub::Meta を利用して比較しています。
また、外部からコールバック関数を登録すれば AnonSub::Typed
以外のインスタンスでも比較できるような設計にしています。
今後の展望
AnonSub::Typed
はもうやることはないかなーという感じですが、
Types::TypedCodeRef
の方は AnonSub::Typed
以外を利用しているパターンでも関数の型を取得できる場合があればコールバック関数を登録しなくても比較できるようにしたいです。
後はテストとドキュメントを充実させたらCPANizeしたいと思っています。
MySQLの INFORMATION_SCHEMA.COLUMNS CHARACTER_MAXIMUM_LENGTH で得られる値についての調査
概要
諸事情によりMySQLのテーブルの文字列型カラムに入る最大値を取得したい欲求が発生し、 INFORMATION_SCHEMA データベースの COLUMNS テーブルの CHARACTER_MAXIMUM_LENGTH*1 を利用することになったので、
CHARACTER_MAXIMUM_LENGTH を参照した時、それぞれのデータ型がどのような値を返すのかのメモを残します。
まとめ
- 文字列型の場合にのみ、そのカラムに入る文字列の長さの最大値が返ってきます。
- サイズが指定できる文字列型(VARCHAR, VARBINARYなど)の場合、指定されたサイズが返ってきます。
- サイズが指定できないような文字列型(TEXTなど)の場合、その型に格納できる文字列の長さの最大値が返ってきます。
- ENUM型、SET型の場合は少し特殊です
- ENUM型の場合、要素の中で最も長い文字列の長さを返します
- SET型の場合、
sum(各要素の文字列の長さ + 1) - 1
と言った風になります(例えば、SUM('A', 'BB', 'CCC')
となっているカラムがあれば、 8が返ってきます)
- 文字列型以外のカラムの型の情報を見ようとすると NULL が返されます
調査内容について
- 環境 : MySQL5.6, MySQL8.0
- 調査のために作ったテーブル
CREATE TABLE sandbox ( `char` CHAR(64), `char_big` CHAR(255), `varchar` VARCHAR(16), `varchar_big` VARCHAR(255), `tinytext` TINYTEXT, `text` TEXT, `mediumtext` MEDIUMTEXT, `longtext` LONGTEXT, `binary` BINARY(32), `binary_big` BINARY(255), `varbinary` VARBINARY(128), `varbinary_big` VARBINARY(255), `tinyblob` TINYBLOB, `blob` BLOB, `mediumblob` MEDIUMBLOB, `longblob` LONGBLOB, `enum` ENUM( 'hoge', 'fugafuga', 'piyopiyopiyo' ), `set` SET( 'foo', 'barbar', 'bazbazbaz' ), `set2` SET( 'wktk', 'hshs', 'blahblahblah', 'orz' ), `date` DATE, `datetime` DATETIME, `timestamp` TIMESTAMP, `time` TIME, `year` YEAR, `tinyint` TINYINT, `smallint` SMALLINT, `mediumint` MEDIUMINT, `int` INT, `bigint` BIGINT, `decimal` DECIMAL, `float` FLOAT, `double` DOUBLE, `bit` BIT, `geometory` GEOMETRY, `point` POINT, `line_string` LINESTRING, `polygon` POLYGON, `geometorycollection` GEOMETRYCOLLECTION, `multipoint` MULTIPOINT, `multilinestring` MULTILINESTRING, `multipolygon` MULTIPOLYGON );
実行結果
SELECT COLUMN_NAME, CHARACTER_MAXIMUM_LENGTH FROM INFORMATION_SCHEMA.COLUMNS WHERE table_name = 'sandbox';
+---------------------+--------------------------+ | COLUMN_NAME | CHARACTER_MAXIMUM_LENGTH | +---------------------+--------------------------+ | char | 64 | | char_big | 255 | | varchar | 16 | | varchar_big | 255 | | tinytext | 255 | | text | 65535 | | mediumtext | 16777215 | | longtext | 4294967295 | | binary | 32 | | binary_big | 255 | | varbinary | 128 | | varbinary_big | 255 | | tinyblob | 255 | | blob | 65535 | | mediumblob | 16777215 | | longblob | 4294967295 | | enum | 12 | | set | 20 | | set2 | 26 | | date | NULL | | datetime | NULL | | timestamp | NULL | | time | NULL | | year | NULL | | tinyint | NULL | | smallint | NULL | | mediumint | NULL | | int | NULL | | bigint | NULL | | decimal | NULL | | float | NULL | | double | NULL | | bit | NULL | | geometory | NULL | | point | NULL | | line_string | NULL | | polygon | NULL | | geometorycollection | NULL | | multipoint | NULL | | multilinestring | NULL | | multipolygon | NULL | +---------------------+--------------------------+
Types::Standard で利用できる、知っておくと便利な型
概要
Perlで型チェックを行うモジュールに Type::Tiny というものがあります。
Type::Tiny には Types::Standard というライブラリがついてきて、いろいろな型が利用可能になるわけですが、
これはその Types::Standard で利用可能な、個人的に知っていると便利だったりわかりやすいコードがかけていいなーと思う型を紹介する記事です。
Enum[@list]
列挙型です。
引数として渡した文字列のリストのうち、いずれかにマッチする値だけを許容する型です。
use Test2::V0; use Types::Standard qw( Enum ); my $constraint = Enum[qw( green blue red )]; ok $constraint->check('green'); ok !$constraint->check('hoge');
(これはほとんどの人が知ってそう)
Maybe[T]
未定義値あるいは型パラメータ(に相当するもの)として渡した型制約を満たす値を受け付ける型です。
use Test2::V0; use Types::Standard qw( Maybe Int ); my $constraint = Maybe[Int]; # 未定義値か整数を受け付ける型制約 ok $constraint->check(1); ok $constraint->check(undef); ok !$constraint->check([]);
これを使うと何が嬉しいかというと、この型制約でチェックする値はnullableであるということが明示的になることです。
例えば、引数としてユーザーのIDを渡し、DBからユーザーの名前を取得するようなメソッドを書くとします。
DBには指定されたユーザーIDに対応するユーザー名がない場合があるわけですが、 Function::Return と Maybe型 で返り値のチェックをさせると、このように未定義値が文字列が返ってくるメソッドであるということが明示的になります。
use Types::Standard qw( Maybe Int ); use Function::Return; sub get_user_name :Return(Maybe[Str]) { my ($self, $id) = @_; return $self->db->select(user => +{ id => $id }); }
Dict[ $key => $constraint, ... ]
列挙された全てのkeyと対応するvalueの型制約を満たすハッシュリファレンスのみを受け付ける型を作ることができるものです。
ネストさせることも可能です。
例えば、 idというキーがInt型の値を持ち、nameというキーがStr型の値を持つようなハッシュリファレンスのみを受け付ける型は、以下のようにして作ることができます。
use Test2::V0; use Types::Standard qw( Dict Int Str ); my $constraint = Dict[ id => Int, name => Str, ]; ok $constraint->check(+{ id => 0, name => 'anon', }); ok !$constraint->check(+{}); ok !$constraint->check(+{ status => 'success' });
ハッシュリファレンスで作られているデータ構造を受け渡ししているようなサブルーチンとかで使うと、どんなデータを受け渡しているのかが明示的になって読みやすいコードになります。
TypeScriptのオブジェクト型と似たようなものだと思います。
Tuple[...]
タプルです。
ちゃんと説明すると、列挙された全ての型制約を満たすような要素をもつ配列リファレンスのみを受け付ける型を作ることができるものです。
ネストさせることも可能です。
例えば、0, 1番目が数値で、2番目が文字列である配列リファレンスのみを受け付ける型は、以下のようにして作ることができます。
use Test2::V0; use Types::Standard qw( Tuple Num Str ); my $constraint = Tuple[Num, Num, Str]; ok $constraint->check([0.4, 10, 'hoge']); ok !$constraint->check([]); ok !$constraint->check(['foo', 'bar', []]);
順序づけられた組であることを明示的にコードに書きたい場合や、手っ取り早くデータ構造を作りたい時などに便利なのではないでしょうか。
Optional[T]
Dict, Tuple と一緒に使う型で、省略可能な要素を作れます。
例えば先ほどのDict型のnameをキーとする組を省略可能にしたい場合は、次のようになります。
use Test2::V0; use Types::Standard qw( Dict Optional Num Str ); my $dict = Dict[ id => Num, name => Optional[Str], ]; ok $dict->check({ id => 0, name => 'anon', }); ok $dict->check({ id => 0 });
Tuple型の最後の要素を省略可能にしたい場合は次のようになります。
use Test2::V0; use Types::Standard qw( Tuple Optional Num Str ); my $tuple = Tuple[Num, Num, Optional[Str]]; ok $tuple->check([0.4, 10, 'hoge']); ok $tuple->check([0.4, 10]); ok !$tuple->check([]); ok !$tuple->check(['foo', 'bar', []]);
(Optional単独でも使えますが、その場合の動作は未定義のようなので使うべきではありません。)
HasMethods[@methods]
引数として渡された全てのメソッドをもつクラスのインスタンスのみ受け付ける型です。
以下のコードでは do_something メソッドをもつインスタンスのみを受け付ける型制約を作っています。
package Foo { sub new { my $class = shift; bless +{}, $class; } sub do_something {} } package Bar { sub new { my $class = shift; bless +{}, $class; } } use Test2::V0; use Types::Standard qw( HasMethods ); my $constraint = HasMethods['do_something']; ok $constraint->check(Foo->new); ok !$constraint->check(Bar->new);
duck typing を利用してメソッドを呼び出している場所があるメソッドの引数のバリデーションに利用するなどすると、どんな引数を渡せばいいのかが明示的なコードが書けてよいです。
use Type::Params qw( compile ); use Types::Standard qw( HasMethods ); sub hoge { state $check = compile(HasMethods['do_something']); my ($foo) = $check->(@_); $foo->do_something(); ... }
ちなみに、 Types::Util の duck_type 関数で同等のことができます。
どちらを使うかは好みになるでしょうか・・・。
おわりに
これらの型を活用することでよりわかりやすいコードが書けると思うのでぜひ活用してみてください。
あと、 Types::Standard には他にも知っているとためになるような型がいろいろあるので、時間があればよく目を通しておくといいかなとおもいます。
例えばtieとかoverloadをよく使っているコードがあるのなら、tieされている値やoverloadされている値しか受け付けない型みたいなのも利用できます。
Perlにおける動的なモジュールロードのメリットとデメリットについて
概要
- Perlにおける動的なモジュールロードのメリットとデメリットについて、そしてどういう場合に動的なモジュールロードを行うといいか、僕が考えていることを記します
- 例のコードや具体例の説明などはPerlの文脈で行っていますが、動的なモジュールロードが可能な言語ならどの言語でも似たような利点と欠点が発生することになると思います
ご指摘などあればコメント欄で教えていただけると幸いです。
動的なモジュールロードを行うことによるメリット
アプリケーションの起動にかかる時間を短縮できる
モジュールをロードするタイミングをコードを実行する直前にまで遅らせることができるので、アプリケーションの起動にかかる時間を短くすることができます。
高速化を意識しているCPANモジュールなどで、このようなコードを見かけることが多い気がしています。
例えば Class::Accessor::Lite
ではこのようなことをしています。
https://metacpan.org/release/Class-Accessor-Lite/source/lib/Class/Accessor/Lite.pm#L7
コード量を少なくできる
例えば、下記のコードのようにたくさんのモジュールを1個1個useするのは結構面倒だったりしますが、動的にモジュールをロードする場合はfor文で一括でロードできちゃったりします。
use MyApp::Foo; use MyApp::Bar; use MyApp::Baz;
use Module::Load qw( load ); # コアモジュールのモジュールローダーです load "MyApp::$_" for qw( Foo Bar Baz );
また、Catalystのモジュールローダーのように*1、useなどモジュールをロードする関数を呼び出さずに、モジュールローダーを介して直接他のモジュールのメソッドを呼び出させることもできます。
この場合、長いモジュール名のモジュールをある程度短い名前で呼び出せるというメリットもあるかなと思います。
use MyApp::Model::Foo; sub hoge { my $self = shift; MyApp::Model::Foo->do_something(); }
use Module::Load qw( load ); sub hoge { my $self = shift; # モジュールローダー(modelメソッド)は MyApp::Model::Foo をまだロードしていない場合ロードして、 # 'MyApp::Model::Foo' という文字列を返す。 # -> 演算子は左側の文字列をパッケージ名としてメソッドを呼びだそうとするので、 # MyApp::Model::Foo->do_something() と同じ処理を行う $self->model('Foo')->do_something(); } sub model { my ($self, $module_name) = @_; my $package_name = 'MyApp::Model::' . $module_name; load $package_name; die $@ if $@; return $package_name; }
モジュールをロードする順番を動的に決めることができる
モジュールロードを実行時に行うと、モジュールをロードする順番を動的に決めることができたり、状況に応じて依存するモジュールを変えれたりします。
例えば(そもそもこのような設計はよくないのですが)、以下のコードのように相互に依存するモジュールがある場合、静的に互いのモジュールをロードしていると、コンパイル時に互いを再帰的にロードしようとしてしまうためか(?)、それぞれのファイルのコンパイル時に Subroutine redefined
の警告が出ます。
Foo.pm
package Foo; use strict; use warnings; use feature 'say'; use Bar; sub hoge { say 'hoge'; } sub fuga { Bar::hogera(); say 'fuga'; } 1;
Bar.pm
package Bar; use strict; use warnings; use feature 'say'; use Foo; sub piyo { Foo::hoge(); say 'piyo'; } sub hogera { say 'hogera'; } 1;
しかし、以下のように依存しているモジュールが必要になる直前に動的にモジュールロードを行った場合、そのような警告は出ません。
Foo.pm
package Foo; use strict; use warnings; use feature 'say'; sub hoge { say 'hoge'; } sub fuga { require Bar; Bar::hogera(); say 'fuga'; } 1;
Bar.pm
package Bar; use strict; use warnings; use feature 'say'; sub piyo { require Foo; Foo::hoge(); say 'piyo'; } sub hogera { say 'hogera'; } 1;
動的なモジュールロードを行うことによるデメリット
依存しているモジュールで何かエラーが起きていても、実際にロードされるまでわからない
依存しているモジュールで何かエラーが起きていても、実際にロードされるまでわからないため、アプリケーションの起動後にSyntax errorなどコンパイル時にわかるようなエラーで一部の機能が使えない・・・といった事態が発生してしまう恐れがあります。
全てのモジュールのテストが書かれていればこのようなデメリットを無視できますが、通常必ず全てのモジュールにテストが用意されていることに期待をすべきではないでしょう。
静的解析を利用したツールの恩恵にあずかれなくなる。また、読みにくいコードになる恐れがある
これはモジュールロード時にモジュール名を動的に組み立てている場合に発生する問題で、動的なモジュールロードによる問題というよりは動的にトークンを組み立てる問題と言えそうですが、動的にモジュールロードを行うときに一緒によく起きがちな問題なので取り上げます。
例えば先ほど上げたCatalyst風のモジュールローダーでモジュールをロードしている場合、静的解析ではどのようなモジュールをロードしているかはわからないため、コードジャンプやメソッド名を補完するようなツールの中には正常に動作しないものがけっこうでてきます。
そのようなツールに特別な対応させることで動作させることは可能かもしれませんが、その分の対応コストがかなりかかるでしょう。
sub hoge { my $self = shift; # modelメソッドがどういうモジュール名を返すのかは, # 実際にコードを実行してみないとわからない! $self->model('Foo')->do_something(); }
また、動的にモジュールロードされていることを知らない人がコードを読んだ場合、依存モジュールがどこで使われているのかがわかりにくくなってしまう恐れがあります。
モジュール間の依存関係がわかりにくくなってしまって、変な設計をしてしまう原因になる
- モジュール間で相互依存が発生している場合も警告がでないので気づけなくなってしまう
- モジュール間の依存関係を機械的に解析することが難しくなる
- 個人的な感覚ですが、コードの途中でモジュールがロードされているより、モジュールのある一箇所で依存しているモジュールがまとめてuseされている方が依存関係がわかりやすいと思います
他にも、CHECK, INITコードブロックに書かれたコードが実行されないなどといった細かい問題などもあります。
所感
僕としては、特に長期的にメンテナンスしていく必要のあるコードや他の人が読むようなコードおいてはデメリットの方が目立ってしまうため、プロダクトではなるべく動的なモジュールロードは行わない方がよい開発体験が得られるのではないかと思っています。
動的なモジュールロードを行ってもよい場面は、個人的に使う本当に使い捨てのスクリプトを書くような場面や、CPANモジュールを作るときにアプリケーションの起動が遅くならないようにしたい場合や、Plackのようなエンジンを作るなどといった場合に限られるのではないでしょうか。
VimでPerlを書くときにctagsを導入して使ってみた
コードジャンプを利用したい場合にctagsを導入するという方法がありますが、最近VimでPerlを書くときに導入してみたところ、非常に良い感じだったので、インストール方法と簡単な使い方を記しておきます。
インストールするものについて
ctagsの実装(?)にはいろいろ種類があるようですが、今も活発に開発が続いている universal-ctags
をインストールするのが良さそうです。
1. インストールする
僕の個人機のOSはUbuntu18.04なので、今回はgithub上にあるレポジトリからcloneしてきて手動でビルドしました。
$ git clone https://github.com/universal-ctags/ctags $ ./autogen.sh $ ./configure $ make $ make install
詳しいことが知りたい場合は Readme や docs/autotools.rst を見てください。 Max OSだとbrewからインストールもできるようです。
2.tagsファイルを生成する
エディタでコードジャンプできるようにするには、まず対象のコードに対してどのようなパターンの時にどのファイルのどの場所にジャンプするのか、みたいなことを定義したtagsファイルを生成する必要があります。
tagsファイルはctagsコマンドで生成できます。
今回はPerlのコードのみを対象にしているので、プロジェクトのルートディレクトリで以下のようなコマンドを実行しtagsファイルを生成します。
$ ctags -R --languages=Perl
実行すると、 tags というファイルが生成されているのが確認できます。
tagsファイルは直接プロジェクトに関係ないので、ついでに .gitignore
に追加しておくといいと思います。
注意点として、tagsファイルはコードに変更がある度に生成し直さないと正しくコードジャンプできなくなるので、コードに変更がある度に再生成する必要があります。
tagsファイルを再生成する方法はいろいろありますが、僕は git で commit した時にフック処理をさせるようにして、commitする度にtagsファイルの再生成を行うようにさせました。
Cartonを利用して開発している場合、 local/
以下に依存モジュールが入っていると思うので、依存モジュールの定義元にもコードジャンプできてより開発が捗ります。
3. Vimのキーバインドについて
- 定義元へのジャンプ :
<C - ]>
- 複数の候補がある場合の定義元へのジャンプ :
<gC - ]>
- 継承してoverrideしているようなモジュールが複数ある場合などに使います
- コードジャンプ前の位置に戻る :
<C - t>
他にもいろいろありますが、とりあえずこれらのキーバインドを覚えとくといいと思います
所感
Perlでコードを書いてると、どんな引数を渡せばよくわからなくて関数の定義元に飛びたい!、というような状況がよくあるのでとても便利です。
しかし、Mooseのシンタクスシュガーやkeywordプラグインを使っているようなモジュールには対応していないので、それらを使っている場合は別の方法でtagsファイルを生成する必要がありそうです。(CPANにそういうライブラリがあがってるっぽいのでそのうち試したい。)
Function::Parameters + Mouse::Util::TypeConstraints = パフォーマンス激強
概要
Perlのサブルーチンの引数バリデーションをするモジュールはいろいろありますが、 Function::Parameters が他の引数のバリデーションを行うモジュールと比べて高速にバリデーションできるという話を聞いて、であれば Mouse::Util::TypeConstraints と組み合わせて使えばとても良いパフォーマンスがでるのではと思い、実際にベンチマークをとってみました。
比較対象は以下のとおりです。
最近良く使われている印象があるモジュールと、何もチェックを行わない場合とで比較してみました。
Smart::Args
と Data::Validator
は、引数にハッシュを渡すほうがリストを渡すよりもパフォーマンスが良い(あるいはほとんど変わらない)ようなので、ハッシュを渡しています。
- 何もチェックしない場合
- Data::Validator 1.07
- Smart::Args 0.14
- Function::Parameters 2.001003 + Type::Tiny 1.004002
- Function::Parameters 2.001003 + Mouse::Util::TypeConstraints v2.5.4
ベンチマークスクリプト
use v5.28; use warnings; use utf8; use DDP; use Benchmark qw( timethese cmpthese ); use Smart::Args qw( args args_pos ); use Data::Validator; sub nomal { my ($id, $name, $pass) = @_; } sub smart_args { args my $id => 'Str', my $name => 'Str', my $pass => 'Str'; } sub data_validator { state $rule = Data::Validator->new( id => 'Str', name => 'Str', pass => 'Str', ); my $args = $rule->validate(@_); my ($id, $name, $pass) = @$args{qw/ id name pass /}; } { use Function::Parameters; use constant Str => Mouse::Util::TypeConstraints::find_or_create_isa_type_constraint('Str'); fun function_parameters_mouse(Str $id, Str $name, Str $pass) { } use Types::Standard qw( Str ); fun function_parameters_types_standard(Str $id, Str $name, Str $pass) { } } my $args = { id => 'mp0liiu', name => 'Maybe_mp0liiu', pass => 'hogehoge!!!', }; my @args = @$args{qw/ id name pass /}; cmpthese( timethese( 100_0000, { nomal => sub { nomal(@args) }, smart_args => sub { smart_args($args) }, data_validator => sub { data_validator($args) }, function_parameters_mouse => sub { function_parameters_mouse(@args) }, function_parameters_types_standard => sub { function_parameters_types_standard(@args) }, } ) );
結果
Rate smart_args data_validator function_parameters_types_standard function_parameters_mouse nomal smart_args 23068/s -- -77% -93% -97% -99% data_validator 99404/s 331% -- -68% -86% -96% function_parameters_types_standard 313480/s 1259% 215% -- -56% -87% function_parameters_mouse 719424/s 3019% 624% 129% -- -69% nomal 2325581/s 9981% 2240% 642% 223% --
感想
Function::Parameters
と Mouse::Util::TypeConstraints
を併用した場合の速度がとても速くて驚きました。
誤差などあると思いますが、何もチェックしない関数の1/3程度の時間でちゃんと引数の個数や型をチェックをしてくれるってすごくないですか???
Function::Parameters
の書き方は微妙に気に入らないところもあるのですが、このパフォーマンスで引数の個数と型をチェックしてくれるのは非常に魅力的なので個人の開発では利用していきたいと思います。
ただ、型チェックオブジェクトを Types::Standard
みたいにクールにかけないので、 Mouse::Util::TypeConstraints
を使う場合も同じようにクールに書けると良さそうかなあ、、、