前回の(1)はこちら から。
Type::Tinyを用いた型制約
型制約とは期待している値の範囲を表し、たとえばNum
という型制約であれば数値全体を表します。できてよいことだけできるのが、良い型制約です。
ここでは、Perlの型制約のType::Tiny
1.004002を紹介します。
Type::Tiny ── Perlのモダンな型制約
Type::Tiny
は型制約とその周辺ツールを同封したモジュールで、TOBYINK によって作られました。WebアプリケーションフレームワークのDancer2
やGraphQL
の型制約として利用されています。
Type::Tiny
には、大きく3つの特徴があります。
ポータビリティが高い
Moose
、Mouse
、Moo
などのクラスビルダの型制約と互換性がある
基本コアモジュールのみに依存している
定義済み型制約ライブラリの使い勝手が良い
パフォーマンスが十分良い
ピュアPerl実装のType::Tiny
でも、Sub::Quote::quote_sub
で型の判定をインライン展開し、速い
Type::Tiny::XS
、Ref::Util::XS
などXSモジュールで必要に応じてスピードアップできる
まずは、Type::Tiny
を直接使ってみましょう。次のコードは偶数の型制約を作っています。偶数判定するコードリファレンスを渡し、$_
は判定したい値です。
use Type::Tiny;
my $even = Type::Tiny->new(
constraint => sub { $_ % 2 == 0 },
);
$even->check(3); # => not ok
$even->check(4); # => ok
Type::Tiny
は表2 の判定メソッドを持ち、Moose(あるいはMouse)::Meta::TypeConstraint
と似た使い勝手になっています。
表2 型制約の判定関連メソッド
メソッド 説明
check($value) : Bool
型制約を満たすか否か真偽値を返す
validdate($value) : Maybe(Str)
型制約を満たさなければエラーメッセージを返す
assert_valid($value) : 1 or die
型制約を満たさなければdie
get_message($value) : Str
与えられた値に対するエラーメッセージ。型制約を満たすか否かは問わない
Type::Libraryで、型制約を再利用可能にする
Type::Tiny
と同時にインストールされるモジュールに、Type::Library
、Type::Utils
、Types::Standard
があります。Type::Library
を利用すれば、型制約の再利用が可能になります。Type::Utils
はdeclare
、as
、where
、enum
といった型制約定義のためのDSL(Domain Specific Language 、ドメイン特化言語)を提供します。そしてTypes::Standard
は、後述の通りInt
、Str
といった基本的な型制約を提供します。これらのモジュールを利用することで、簡潔に型制約を宣言できます。
次のコードは、偶数と血液型の型制約を定義しています。
package MyType;
use strict;
use warnings;
use Type::Library -base;
use Type::Utils;
use Types::Standard -types;
# 偶数
declare 'Even',
as Int,
where { $_ % 2 == 0 };
# 血液型
enum 'Blood', ['A', 'B', 'AB', 'O'];
1;
型制約Even
、Bloodは
、宣言した名前でエクスポートでき、スッキリと記述できます。
use Test::More;
use MyType qw(Even Blood);
ok not Even->check(3);
ok Even->check(4);
ok Blood->check('A');
ok not Blood->check('C');
done_testing;
エクスポートオプションはほかにも豊富にあるので、Type::Library
のドキュメント を参照してください。
DSLを使わずType::Libraryを利用する
理解のために、DSLを使わず上述の型制約と等価のパッケージを用意してみます。
package MyTypeWithoutDSL;
use strict;
use warnings;
use Type::Library -base;
use Type::Tiny;
use Types::Standard qw(Int Str);
__PACKAGE__->meta->add_type(
Type::Tiny->new(
name => 'Even',
parent => Int,
constraint => sub { $_ % 2 == 0 }
)
);
__PACKAGE__->meta->add_type(
Type::Tiny->new(
name => 'Blood',
parent => Str,
constraint => sub { m!\A(?:A|B|AB|O)\z! }
)
);
__PACKAGE__->meta->make_immutable;
Type::Library
は、__PACKAGE__->meta
に型制約の情報を保存し、Type::Utils
はこのメタオブジェクトを隠蔽( いんぺい ) するDSLを提供していることがわかります。たとえば、このメタオブジェクトを利用して、__PACKAGE__->type_names
で定義した型制約の一覧を取り出すことができます。
定義済み型制約ライブラリ
既存の定義済みの型制約ライブラリを紹介します。独自の型制約を定義する際、基本的には定義済みの型制約の組み合わせで表現することになります。
Types::Standard──組み込みの基本的な型制約ライブラリ
よく使う型制約を詰め合わせしたTypes::Standard
の利便性は高いです。これもType::Tiny
に同封されます。Types::Standard
は、まずPerl 5の基本型制約をMoose
、Mouse
と同様に提供します。
Any
Item
Bool
Maybe[`a]
Undef
Defined
Value
Str
Num
Int
ClassName
RoleName
Ref
ScalarRef[`a]
ArrayRef[`a]
HashRef[`a]
CodeRef
RegexpRef
GlobRef
FileHandle
Object
この基本型と組み合わせ、ほかに構造やオブジェクトなどの型制約も利用できます。
以下は、Map
、Dict
、Tuple
の例です。JSONのような構造の型制約を作る場合に便利です。
# HashRef のキー、値が、Str、Int か
my $Map = Map[Str, Int];
ok $Map->check({ a => 1, b => 2 });
ok not $Map->check({ a => 1, b => 'aaa' });
# HashRefで、かつ
# nameキーに対する値がStr、ageキーに対する値がInt
my $Dict = Dict[name => Str, age => Int];
ok $Dict->check({ name => 'foo', age => 2 });
ok not $Dict->check({ name => 'bar', age => 'AA' });
# Optional で一部のキーがなくてもよい
{
my $Dict = Dict[name => Str, id => Optional[Int]];
ok $Dict->check({name => 'foo', id => 1});
ok $Dict->check({name => 'bar'});
ok not $Dict->check({name => 'bar', id => 'AAA'});
}
# ArrayRef で値がそれぞれ Str, Int
my $Tuple = Tuple[Str, Int];
ok $Tuple->check(['foo', 1]);
ok not $Tuple->check(['foo', 'aaa']);
ok not $Tuple->check(['foo', 1, 123]);
次に、InstanceOf
とHasMethods
の利用例です。HasMethods
はダックタイピングに利用します。
# Foo または Bar のインスタンスか否か
my $InstanceOf = InstanceOf['Foo', 'Bar'];
ok $InstanceOf->check(bless {}, 'Foo');
ok $InstanceOf->check(bless {}, 'Bar');
ok not $InstanceOf->check(bless {}, 'Baz');
# check, get_message メソッドを持つか
my $HasMethods = HasMethods['check', 'get_message'];
{
use Type::Tiny;
use Mouse::Meta::TypeConstraint;
ok $HasMethods->check(Type::Tiny->new);
ok $HasMethods->check(
Mouse::Meta::TypeConstraint->new
);
}
続いて、Enum
、Overload
、Tied
の例です。tie
を用いることで変数と型制約を結び付け、変数の変更時 にも型制約の判定が行えるのはおもしろいです。
# Hoge, Fugaのうちのいずれか
my $Enum = Enum['Hoge','Fuga'];
ok $Enum->check('Hoge');
ok $Enum->check('Fuga');
ok not $Enum->check('Boo');
# 指定した演算子がオーバーロードされているか
my $Overload = Overload['&','|','~','>','<'];
ok $Overload->check(Type::Tiny->new);
# tie されているか否か / 値に型制約をtieできる
tie my $tiestr, Str;
ok \$tiestr ~~ Tied;
$tiestr = 'hello'; # ok
eval { $tiestr = {} }; # die
ok $@;
そのほかの型制約ライブラリ
型制約ライブラリには、ほかに次のようなものがあります。
Types::Common::String
文字列関連の型制約
UpperCaseStr、LowerCaseStrといった大文字、小文字の型制約
StrLength[min, max]といった文字列長による型制約
Types::Common::Numeric
数字関連の型制約
PositiveNum、NegativeNum、PositiveInt、NegativeIntといった正負の型制約
NumRange[min,max]、IntRange[min,max]といった数値区間の型制約
いくつもの型制約のライブラリをuse
することは手間ですので、Type::Utils#extends
でアプリケーションで利用する型制約をまとめておくと便利です。
package MyTypeExtended;
use Type::Library -base;
use Type::Utils -all;
BEGIN {
extends qw(
Types::Standard
Types::Common::Numeric
Types::Common::String
);
}
declare 'MyRange',
as StrLength[0,191],
our @EXPORT = __PACKAGE__->type_names;
1;
これで、自前で用意したMyRange
もTypes::Standard
のStr
も、MyTypeExtended
をuse
するだけで利用できます。
use MyTypeExtended -types;
Str->check('foo');
MyRange->check('bar');
型制約のライブラリには、Types::URI
、Types::Path::Tiny
、Types::UUID
といった用途がはっきりしたものもあります。たとえばTypes::URI
であれば文字列からURI
オブジェクトへの暗黙的な変換が行え、わかりきったURI
オブジェクトへの変換を省け、型制約に振り回されにくくなります。こういった変換を、型強制と呼びます。
<続きの(3)はこちら 。>
特集1
イミュータブルデータモデルで始める
実践データモデリング
業務の複雑さをシンプルに表現!
特集2
いまはじめるFlutter
iOS/Android両対応アプリを開発してみよう
特集3
作って学ぶWeb3
ブロックチェーン、スマートコントラクト、NFT