Masteries

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

今, Smart::Args::TypeTinyが熱い!?

この記事は, 「Perl Advent Calendar 2017」の24日目の記事です.

qiita.com

昨日は, id:papix の「VimにおけるPerl関連のスニペットを晒してみる 〜2017年版〜」でした.

papix.hatenablog.com

Smart::Argsは便利

Perlで, 関数に渡ってきた引数のチェック(バリデーション)をするのであれば, Smart::Argsを使うのが一般的だと思います.

metacpan.org

Smart::Argsを使えば, 次のように関数に渡ってくる引数をチェックできます. ここでは, funcという関数に対して, fooというキーでInt(数値), barというキーでStr(文字列)が渡ってくることを指定しています.

use strict;
use warnings;
use utf8;

use Smart::Args qw/ args /;

func(foo => 1,     bar => 'bar');
func(foo => 'foo', bar => 'bar');

sub func {
    args
        my $foo => 'Int',
        my $bar => 'Str';
}

最初の呼び出しは, foo1という数値, bar'bar'という文字列がそれぞれ渡っているので大丈夫ですが, 2回目の呼び出しは, foo'foo'という文字列が渡っています. 2回目のfuncを呼び出すと, Perlは次のようなエラーを返します:

'foo': Validation failed for 'Int' with value foo at /path/to/lib/Smart/Args.pm line 179.
        Smart::Args::_validate_by_rule("foo", 1, "foo", "Int", SCALAR(0x7fccba8e8df8)) called at /path/to/lib/Smart/Args.pm line 63
        Smart::Args::args(undef, "Int", undef, "Str") called at smart_args.pl line 11
        main::func("foo", "foo", "bar", "bar") called at smart_args.pl line 8

Smart::Argsを使えば, 「関数に渡ってきた引数のチェック」と, 「変数の宣言」が同時に出来るので, とても便利です.

Smart::Args::TypeTiny

そんな中, 最近Smart::Args::TypeTinyというモジュールが公開されました. 作者は id:akiym さん.

metacpan.org

名前からわかる通り, 型にTypeTinyを使った*1Smart::Argsライクなバリデーターなのですが, Smart::Argsとの「IMCOMPATIBLE CHANGES」が, かなり痒い所に手が届くようになっていて, 個人的に注目しています.

期待していないパラメータが渡されるとエラーになる

例えば, 次のコードのように, func関数はfooというキーでIntが渡ってくることを期待しているとします.

use strict;
use warnings;
use utf8;

use Smart::Args qw/ args /;

func(foo => 1, bar => 'bar');

sub func {
    args
        my $foo => 'Int';
}

ここで, Smart::Argsの場合, funcfooに加えてbarというキーで値を渡した場合, 次のようにエラーではなく警告が発生します.

unknown arguments: bar at smart_args.pl line 10.
        main::func("foo", 1, "bar", "bar") called at smart_args.pl line 7

一方, Smart::Args::TypeTinyであれば, 出力される文言は同じですが, 警告ではなくエラーになります.

use strict;
use warnings;
use utf8;

use Smart::Args::TypeTiny qw/ args /;

func(foo => 1, bar => 'bar');

sub func {
    args
        my $foo => 'Int';
}

optionalundefが渡ってくることも許容する

Smart::Argsでは, 次のようにして, 引数をoptional(任意で渡すことが出来る)にすることが出来ます.

use strict;
use warnings;
use utf8;

use Smart::Args qw/ args /;

func(); # (1)
func(foo => 1); # (2)
func(foo => undef); # (3)

sub func {
    args
        my $foo => { isa => 'Int', optional => 1 };
}

このとき, funcを呼び出す時にfooをキーとして値を渡されなければ, funcにおける$fooundefになります(コード中の, (1)のパターン). もちろん, (2)のように, fooをキーとして値を渡せばfuncにおける$fooは渡された値(この場合は1)になりますし, ここでIntではない値を渡せばエラーになります. 問題は, (3)のようにoptionalな引数にundefを渡した場合で, この時Smart::Argsではエラーが発生します.

'foo': Validation failed for 'Int' with value undef at /path/to/lib/Smart/Args.pm line 179.
        Smart::Args::_validate_by_rule(undef, 1, "foo", HASH(0x7fe8cf89f510), SCALAR(0x7fe8cf80e7f8)) called at /path/to/lib/Smart/Args.pm line 63
        Smart::Args::args(undef, HASH(0x7fe8cf89f510)) called at smart_args.pl line 12
        main::func("foo", undef) called at smart_args.pl line 9

一方, Smart::Args::TypeTinyであれば, (3)のようにoptionalな引数にundefを渡してもエラーにならず, funcにおける$fooの値はundefとして処理を続けることができます.

defaultにサブルーチンリファレンスを渡すことで, 必要な時だけ評価することができる

Smart::ArgsもSmart::Args::TypeTinyも, 次のようにdefaultを指定することで, その値が渡ってこなかった時のデフォルト値を指定することができます. そしてこのとき, 何かしらのサブルーチンを呼び出してデフォルト値を埋めることが出来ます.

use strict;
use warnings;
use utf8;

use Smart::Args qw/ args /;

func(); # (1)
func(foo => 1); # (2)

sub func {
    args
        my $foo => { isa => 'Int', default => create_value() };

    print "$foo\n";
}

sub create_value {
    print "called!\n";
    return 123;
}

上のコードでは, funcを呼び出す時にfooというキーで値を渡さなかった場合, create_valueという関数を実行して, その結果をデフォルトの値にする... という挙動になります. このコードを実行すると, Smart::ArgsもSmart::Args::TypeTinyも, 次のような結果になります.

called!
123
called!
1

fooというキーを指定せずにfuncを呼び出した場合, つまり(1)の場合はもちろんのこと, fooというキー指定した(2)の場合でも, create_valueの呼び出しが発生してしまっています. 万が一, create_valueが多少時間のかかる処理であった場合, create_valueの呼び出しが不要であっても毎回呼び出されてしまうので, パフォーマンスの面でも気になって来るかもしれません.

一方, Smart::Args::TypeTinyでは, defaultに対してサブルーチンリファレンスを渡すことで, 「必要な時だけ」デフォルト値を生成することが可能です.

use strict;
use warnings;
use utf8;

use Smart::Args::TypeTiny qw/ args /;

func();
func(foo => 1);

sub func {
    args
        my $foo => { isa => 'Int', default => sub { create_value() } };

    print "$foo\n";
}

sub create_value {
    print "called!\n";
    return 123;
}

このコードを実行すると,

called!
123
1

このような出力になります. create_valueが, 1度しか呼び出されていないことがわかります.

まとめ

最近個人的に注目している, Smart::Args::TypeTinyの紹介をしました. Smart::Argsを使っていない人はもちろんのこと, 既に使っている方も, Smart::Args::TypeTinyに置き換えていく価値は結構ありそうでは? と思っています.

*1:Smart::Argsは型にMouseを使っている