Perlの型づくりチートシート(2023-12-07 updated)
この記事はPerlアドベントカレンダー の7日目の記事です。
Perlで型制約をつくるType::Tinyというモジュールがあります。Moose,Mouse,Mooといったクラスビルダーの型制約と互換性があり、加えて依存も少ないので、使いやすいモジュールだと思います。
型制約といってもType::Tinyは本質的には難しいことはしていなくて、値が期待通りか確認し、期待通りでなかったときフィードバックをする機能を備えたオブジェクトです。*1
ここでは、このType::Tinyを利用して、様々な型制約を作っていきます。
前置き
Type::Tiny 2.004000 を利用しています。Type::Tinyはこんな感じで使えます。
use v5.38; # Type::Tinyに同封されてるTypes::Commonにはよく利用される型制約が詰まっています。 # 次のように書くと、定義している型制約をすべてimportできます use Types::Common -types; # こんな感じで使えます sub add($x, $y) { # 引数がIntでなければ、エラーになります die Int->get_message($x) unless Int->check($x); die Int->get_message($y) unless Int->check($y); $x + $y; }
原始的な型制約
文字列
Str->check("hello"); # ok Str->check({}); # ng Str->check(123); # ok / perlらしいところ
整数値
Int->check(123); # ok Int->check("hello"); # ng Int->check("123"); # ok / perl らしいところ
真偽値
Bool->check(1); # ok Bool->check(!!1); # ok Bool->check(0); # ok !Bool->check("hello"); # ng
Types::Commonには、他に PositiveInt, Numといった型制約もあります。
型に条件を加える
True
# Bool かつ !!$_ が真 (!! はbanban演算子で、$_ には与えられた値が入ります) use constant True => Bool & sub { !!$_ }; True->check(1); # ok True->check(!!1); # ok True->check(0); # ng True->check("hello"); # ng
文字列に長さの制約を加える
# Str かつ 1文字以上 use constant NonEmptyStr => Str & sub { length $_ > 0 }; ok NonEmptyStr->check('hello'); # ok ok NonEmptyStr->check(''); # ng
尚、自前で定義しないでも、Types::Common
にNonEmptyStrは入ってます。
構造化する
配列やハッシュの型制約も作れます。
辞書型
use constant User => Dict[ name => Str, age => Int, ]; User->check({ name => 'taro', age => 20 }); # ok User->check({ }); # ng
配列型
指定した型を要素に持つ配列かどうかチェックしてます。
use constant Users => ArrayRef[User]; Users->check( [ { name => 'taro', age => 20 }, { name => 'hana', age => 22 }, ] ); # ok Users->check([{name => 'taro', age => 20}]); # ok Users->check([]); # ok
タプル型
use constant Friends => Tuple[User, User]; Friends->check( [ { name => 'taro', age => 20 }, { name => 'hana', age => 22 }, ] ); # ok Users->check([{name => 'taro', age => 20}]); # ng Users->check([]); # ng
型制約と型制約を掛け合わせる
ユニオン型
use constant ErrorA => InstanceOf['Error'] & sub { $_->{name} eq 'A' }; use constant ErrorB => InstanceOf['Error'] & sub { $_->{name} eq 'B' }; # Resultオブジェクト または ErrorA または ErrorB use constant Result => InstanceOf['Result'] | ErrorA | ErrorB;
InstanceOf['XXX']
は、XXXオブジェクトかどうか判定する型制約です。
交差型
use constant LongHair => Hair & sub { $_->length > 10 }; use constant BronzeHair => Hair & sub { $_->color eq 'bronze' }; # LongHair かつ BronzeHair use constant LongBronzeHair => LongHair & BronzeHair
ここまで条件を加える書式で、 Type & sub { ... }
と書いてきましたが、これも交差型です。内部的には、右項のコードリファレンスをType::Tinyに自動で昇格し、交差型にしてます。
応用
関数の引数や返り値のチェックに利用する
use constant DEBUG => $ENV{PLACK_ENV} ne 'deployment'; sub add($x, $y) { if (DEBUG) { # 引数$x, $y がIntでなければ、例外が投げられる Int->assert_valid($x); Int->assert_valid($y); } my $result = $x + $y; if (DEBUG) { Int->assert_valid($result); } return $result; }
DEBUGが有効でなければ、定数の畳み込みがなされて、次のコードと同等になり、プロダクト環境でパフォーマンス劣化しないで済みます。
sub add($x, $y) { my $result = $x + $y; return $result; }
パターンマッチ
該当した型制約に関する処理を走らせる。
use Type::Utils qw(compile_match_on_type); use constant Japanese => Str & sub { $_ eq 'Japanese' }; use constant English => Str & sub { $_ eq 'English' }; use constant Lang => Japanese | English; sub hello($lang) { if (DEBUG) { Lang->assert_valid($lang); } state $code = compile_match_on_type( Japanese, sub { 'こんにちは' }, English, sub { 'Hello' }, ); $code->('Japanese'); # こんにちは }
型関数
引数に型制約を受け取り、型制約を返す関数
sub Foo($T) { Dict[ foo => $T, ] } Foo(Int); # Dict[foo => Int];
*1:内部的には、パフォーマンス、互換性、スコープ拡大、型表現力強化ののために様々なことをしている巨大モジュールになっていますが