Masteries

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

忘備録: Smart::Args::TypeTinyのdefaultとoptionalについて

metacpan.org

Smart::Args::TypeTinyには, defaultoptional というパラメータがあります.

default

default は, 引数が渡らなかった時のデフォルト値を指定するパラメータです.

use strict;
use warnings;

use feature qw(say);
use Smart::Args::TypeTiny qw(args);

sub test {
    args
        my $arg => { isa => 'Str', default => 'bar' };

    return $arg // 'undef';
}

say test(arg => 'foo'); # => 'foo'
say test(); # => 'bar' ( default が使われる )
say test(arg => undef); # => Type check failed in binding to parameter '$arg'; Undef did not pass type constraint "Str" (エラー)

なお, isaMaybe の場合はこうなります:

use strict;
use warnings;

use feature qw(say);
use Smart::Args::TypeTiny qw(args);

sub test {
    args
        my $arg => { isa => 'Maybe[Str]', default => 'bar' };

    return $arg // 'undef';
}

say test(arg => 'foo'); # => 'foo'
say test(); # => 'bar'
say test(arg => undef); # => 'undef'

まとめると, default のパラメータがあるときは,

  • 引数としてundef以外を渡す ... 渡した値が使われる (isaを満たさない場合は当然エラーになる)
  • 引数を渡さない ... default が使われる
  • 引数としてundefを渡す
    • isaMaybeである ... undef がそのまま使われる (default は使われない)
    • isaMaybeでない ... isa を満たさないのでエラーになる

...という挙動になります.

optional

一方, optional はその名の通り, オプショナルであることを指定するパラメータです.

use strict;
use warnings;

use feature qw(say);
use Smart::Args::TypeTiny qw(args);

sub test {
    args
        my $arg => { isa => 'Str', optional => 1 };

    return $arg // 'undef';
}

say test(arg => 'foo'); # => 'foo'
say test(); # => 'undef' (`optional` なので引数を渡さなくても問題ない. その場合は undef となる)
say test(arg => undef); # => 'undef' (明示的に引数として`undef`を渡しても問題ない)

まとめると, optional のパラメータがあるときは,

  • 引数としてundef以外を渡す ... 渡した値が使われる (isaを満たさない場合は当然エラーになる)
  • 引数を渡さない ... エラーにならず, undef として扱われる
  • 引数としてundefを渡す ... (isaを満たしていなくても)エラーにならず, そのまま undef として扱われる

defaultoptional

では, defaultoptionalのどちらも指定した場合はどうなるでしょうか.

use strict;
use warnings;

use feature qw(say);
use Smart::Args::TypeTiny qw(args);

sub test {
    args
        my $arg => { isa => 'Str', optional => 1, default => 'bar' };

    return $arg // 'undef';
}

say test(arg => 'foo'); # => 'foo'
say test(); # => 'bar' (`optional` なので引数を渡さなくても問題ない. その場合は `default` で指定された値となる)
say test(arg => undef); # => 'undef' (明示的に `undef` を渡しても問題ない)

この挙動, よくよく見ると2番目に紹介したdefaultを指定 + isaMaybeと同じですね. つまり1つの挙動(optionalな引数で, undefを受け取れて, デフォルト値を指定したい)について, 2通りの書き方がある, ということです.

papixの意見

...とはいえ同じ挙動を複数の書き方ができる, というのは混乱を呼びそうです. どちらかに寄せよう! という合意を取れるといいのかな, と思います. 自分なら, こういう場合は前者(defaultを指定 + isaMaybeにする)として, defaultoptionalは同時に使わない... というふうにするのがわかりやすいんじゃないかな, と思いました.

というわけで以上です, すいませんがオチはないです...

Perlの未定義動作100連発

PHPカンファレンス福岡2023に参加して高まってきたので, Perlの未定義動作100連発というエントリを書きます. なおperldocと実際の挙動については, 5.36.0のものを利用しています.

その1: スカラコンテキストでsort

sort はリストの中身をソートするので, 当然(ソートした結果を受け取るために)リストコンテキストで呼び出す必要があります. 「そんなことしないだろ...」と思いますが, もしスカラコンテキストで呼び出した場合の振る舞いは未定義です.

perldoc.jp

スカラコンテキストでは、sort の振る舞いは未定義です。

実際にやってみました:

コード

use strict;
use warnings;
use DDP;

my $sorted = sort { $a <=> $b } (1, 3, 2, 4);
p $sorted;

結果

Useless use of sort in scalar context at if.pl line 5.
undef

なんとなく, 「ソートした結果の先頭が入ってくるのかな...?」と思っていたけど, undef になるようでした. 「Useless use of sort in scalar context」という警告も出てきますし, まあこういうコードを書いてしまってそのままにしてしまう, ということは少ないでしょう.

その2: my + 後置if

詳しくは id:t_kyt *1さんの以下のエントリを読んでください:

developer.hatenastaff.com

perldoc.jp

注意: (my $x if ... のような) 条件構造やループ構造で修飾された my state,our 文の振る舞いは 未定義 です。 my 変数の値は undef かも知れませんし、以前に代入された値かも 知れませんし、その他の如何なる値の可能性もあります。 この値に依存してはいけません。 perl の将来のバージョンでは現在のバージョンとは何か違うかも知れません。 ここには厄介なものがいます。

実際にやってみました:

コード

use strict;
use warnings;
use DDP;

my $num1 = 123 if 1;
my $num2 = 345 if 0;

p $num1;
p $num2;

結果

123
undef

未定義動作ではありますが, 「まあそうなるよね」という結果になりました. とはいえ, 未定義動作なのでmyと後置ifは組み合わせないようにしましょう. ちなみに, 代入のない単なるmyに後置ifを書いた場合は次のようなエラーになります:

コード

use strict;
use warnings;
use DDP;

my $num1 if 1;

結果

This use of my() in false conditional is no longer allowed at if.pl line 6.

その3: forループの返り値

「forループの返り値...?」となりますよね. まずは perldoc を見てみましょう:

perldoc.jp

洞察力のある Perl ハッカーは、for ループに返り値があり、この値は ループを do ブロックで包むことによって捕捉できることに 気付くかもしれません。 この発見に対する報奨は、この警告的なアドバイスです: for ループの返り値は 未定義で、予告なしに変更されることがあります。 これに依存してはいけません。

というわけで, forをdoで包んで, 強引に返り値を取得してみましょう:

コード

use strict;
use warnings;
use DDP;

my $ret = do {
    for (1..10) {
        # ...
    }
};
p $ret;

結果

""

...というわけで, 強引にforの返り値を取得すると空文字列が返る, みたいです. 「forをdoで囲むことはないやろ...?」と思われるかもしれませんが, 次のコードは有り得そうです:

コード

use strict;
use warnings;
use DDP;

my $ret = func();
p $ret;

sub func {
    for (1..10) {
        # ...
    }
}

結果

""

この場合, 関数funcにはreturnがないので, 関数の返り値として一番最後に評価されるforの返り値が使われます. もし何かしらの理由で返り値を利用することになった場合, 意図せず空文字が返ってきてびっくり!!! ということになりそうです. こういう事例を防ぐためにも, 明示的にreturnを書くのがいいな, と最近思いつつあります.

コード

use strict;
use warnings;
use DDP;

my $ret = func();
p $ret;

sub func {
    for (1..10) {
        # ...
    }
    return undef;
}

結果

undef

いかがでしたか?

100連発といいつつ, まずは3連発でご紹介しました. Perlの未定義動作については, 今後また折を見て紹介していこうと思います.

*1:2023/06/28: 公開時, 記事の投稿者を誤って id:akiym さんと記載していました. お詫びして訂正いたします.

「吉祥寺.pm 33」でYAPC::Kyotoについて全てお話しました

吉祥寺.pm33【吉祥寺でのオフライン開催!!!】 - connpass

久々の吉祥寺.pmですね. 今回はYAPC::Kyotoを題材として, 勉強会運営の"リアル"みたいな(?)お話をしました.

ここ半年くらいは本当にYAPC::Kyoto 2023のことをずーっと考えていたので, こういう勉強会で発表する技術ネタの弾がないような気がしていて良くないですね. 次の吉祥寺.pmまでには残弾増やして披露したいなと思いました.

【Q】Tengでsingleを使って1件だけレコードを引くとき, 明示的に `LIMIT` を指定する必要はあるの?

【A】指定しなくても大丈夫です

github.com

実装を見ると, single を呼んだらその内部で自動的に LIMIT 1 になるように設定をしてくれます(強制的に上書きする). 逆に言うと, single を呼ぶ時に { limit => 100 } みたいなオプションを渡しても, LIMIT 100みたいなクエリは発行されず, LIMIT 1になるように上書きされる, ということです(まあ, そんなことする人いないでしょうが...).

忘備録: Smart::Args::TypeTinyの`default`で時間がかかる処理を呼ぶ時はサブルーチンリファレンスで渡した方が理想的

Perlの忘備録というか何というか... まあタイトルにある通りです. 或いはSmart::Args::TypeTinyのIMCOMPATIBLE CHANGES WITH Smart::ArgsのDefault parameter can take coderef as lazy valueを読みましょう.

use strict;
use warnings;

use Smart::Args::TypeTiny qw(args);

sub test {
    args
        my $arg1 => { isa => 'Str', optional => 1, default => sub { generator('from test1') } },
        my $arg2 => { isa => 'Str', optional => 1, default => generator('from test2') };

    return 1;
}

sub generator {
    my ($message) = @_;

    warn $message;

    return $message;
}

warn "case 1";
test();
warn "case 2";
test(arg1 => 'test');
warn "case 3";
test(arg2 => 'test');
warn "case 4";
test(arg1 => 'test', arg2 => 'test');

これを実行すると, 次のような結果になります(見やすくなるように改行を入れています):

case 1 at smart-args.pl line 22.
from test2 at smart-args.pl line 17.
from test1 at smart-args.pl line 17.
args: from test1, from test2 at smart-args.pl line 11.

case 2 at smart-args.pl line 24.
from test2 at smart-args.pl line 17.
args: test, from test2 at smart-args.pl line 11.

case 3 at smart-args.pl line 26.
from test2 at smart-args.pl line 17. # ← case 3 では arg2 が渡っていてデフォルト値が不要なのに, generator が呼び出されている
from test1 at smart-args.pl line 17.
args: from test1, test at smart-args.pl line 11.

case 4 at smart-args.pl line 28.
from test2 at smart-args.pl line 17. # ← case 4 では arg2 が渡っていてデフォルト値が不要なのに, generator が呼び出されている. arg1 も同様だけど, generator は呼び出されていない
args: test, test at smart-args.pl line 11.

...要するに, default => generator() のようにオプションを指定すると, 引数が渡ってこようが渡ってこまいが毎回generator()を実行しますが, サブルーチンリファレンスで渡すと引数が渡ってこなかった時だけ(サブルーチンリファレンスを呼び出す形で)generator()を実行してデフォルト値を生成するようになっています. 1とか2とかを渡すならともかく, generator() で行われる処理の内容によっては, チリツモで処理時間が伸びていきそうなので, 気をつけたいところです.

ちなみに...

Smart::Argsでdefaultにサブルーチンリファレンスを渡すと, サブルーチンリファレンスそのものがデフォルトの値になりますので, 注意しましょう:

case 1 at smart-args.pl line 22.
from test2 at smart-args.pl line 17.
args: CODE(0x14d0c6568), from test2 at smart-args.pl line 11.