Perl で簡単に総称型を作れるユーティリティを作った
Perl で総称型を作ろうとすると Type::Tiny にいろんなオプションを渡さなければいけなくて結構面倒だったりします。
例えば、型引数を与えたときにその型引数の制約を満たす値のみを入れられる Queue の型を作る総称型を作ろうとすると、次のようになります。
package Queue { use Moo; use Types::Standard -types; has data => ( is => 'ro', isa => ArrayRef, default => sub { [] }, ); sub push { my ($self, $data) = @_; push $self->data->@*, $data } sub pop { my $self= shift; shift $self->data->@*; } } use Type::Tiny; use Types::Standard -types; use Test2::V0; sub Queue(;$) { my $type_params = shift; my $type = Type::Tiny->new( parent => InstanceOf['Queue'], name_generator => sub { my ($name, $type_param) = @_; 'Queue[' . $type_param . ']'; }, constraint_generator => sub { my $type_param = shift; return InstanceOf['Queue'] unless defined $type_param; my $DataType = ArrayRef[$type_param]; sub { my $queue = shift; # Queue->data の型が型引数の制約を満たす値の配列である場合のみ受け付ける $DataType->check($queue->data); }; }, ); $type->parameterize(@$type_params); } my $QueueInt = Queue[Int]; ok $QueueInt->check( Queue->new(data => [0 .. 10]) ); ok !$QueueInt->check( Queue->new(data => [(undef) x 10]) ); done_testing;
Type::Tiny に詳しければこんな感じの総称型を作るのは難しくはないんですが、記述量が多かったりあまり見栄えがよくなかったりするので、このようなコードに何回も登場してもらいたくなかったりします。
また、他のクラスでも総称型を作りたい場合同じような記述を何回もすることになるのであまりイケてないなあという気持ちにもなるわけですね。
そこで簡単に総称型を作れるユーティリティを作ることにしました。
できること
まずこのユーティリティでは class_generics
という関数が提供されています。
これは与えられた型引数で特定のアトリビュートの型をチェックするような型を簡単に作れるユーティリティです。
class_generics
で先程作ったような Queue の総称型を作ると次のようになります。
use Types::Standard -types; use Type::Utils::Generics qw( class_generics T ); sub Queue(;$); class_generics Queue => ( class_name => 'Queue', attributes => +{ data => ArrayRef[ T(0) ] }, ); my $QueueInt = Queue[Int];
class_name
にクラス名、 attributes
に型引数で置き換えたいアトリビュートの情報を渡すことで総称型を作れます。
attributes
で渡している値が奇妙な見た目をしているので詳しく解説すると、 attributes
には型引数でチェックしたいアトリビュートの名前と型引数で置き換えたい位置を示した型(ここではこれを型テンプレートと呼ぶことにします)のペアを渡します。
T(0)
という部分が型引数で置き換えられる部分です。
他の言語で総称型を作ると Queue<T>
といったふうになりますが、このときのパラメータ T
と同じ概念のものです。
この型引数で置き換えられる部分を表す T
という関数は、後から置き換える用の意味のある型チェックをしない型オブジェクトを生成する関数で、引数でどの順番の型引数と置き換えるかを指定します。
上の例では引数に 0
を与えているので 0番目の型引数と置き換えるといった感じに動作します。
他にも型引数の順番の指定方法など考えてみたのですが、同じ型引数で別のアトリビュートの型を置き換えたい場合などを考えると他にいい方法を思いつかず、このような見た目になってしまいました。
その他には sub_generics
という型が提供されています。
これは与えられた特定の場所のパラメータの型や引数の型を型引数で置き換えた関数の型を簡単に作れるユーティリティです。
例えば配列の中から同じ値の要素を探し返す関数があったとして、その配列の要素や返り値の型を型引数で指定できるような総称型を作りたい場合は次のようになります。
use Types::Standard -types; use Type::Utils::Generics qw( sub_generics T ); use Sub::WrapInType qw( wrap_sub ); sub find { my ($array, $val) = @_; for my $elem (@$array) { return $elem if $elem == $val; } return; } sub Find($); sub_generics Find => ( params => [ ArrayRef[T(0)], T(0) ], isa => Maybe[T(0)], ); use Test2::V0; my $FindInt = Find[Int]; # = TypedCodeRef[ [ Array[Int], Int ] => Int ] ok $FindInt->check(wrap_sub [ Array[Int], Int ] => Int, \&find);
型テンプレートの部分は class_generics
の attributes と同じように動きます。
できないこと
他の言語だとクラスの総称型を作るとメソッドの引数の方や返り値の型もパラメータ化できますが、それをやろうとするとインスタンスごとにメソッドの型定義を変える必要があり、CPU的にもメモリ的にもコストがかかりすぎて現実的ではないなと思い断念しました。
今後の展望など
記事を書いていて思ったのですが、sub_generics
のメソッド版の method_generics
というユーティリティ関数もあったほうが便利そうなので作りたいと思います。
見た目が奇妙なことと、型オブジェクトを本来想定されていないような使い方をしていることから CPAN にあげるかはかなり迷っている状況です。。