Perl Hackers Hub

第52回Perlで堅牢な開発―構文チェック、静的検査、型制約(2)

前回の(1)こちらから。

Type::Tinyを用いた型制約

型制約とは期待している値の範囲を表し、たとえばNumという型制約であれば数値全体を表します。できてよいことだけできるのが、良い型制約です。

ここでは、Perlの型制約のType::Tiny 1.004002を紹介します。

Type::Tiny ─⁠─ Perlのモダンな型制約

Type::Tinyは型制約とその周辺ツールを同封したモジュールで、TOBYINKによって作られました。WebアプリケーションフレームワークのDancer2GraphQLの型制約として利用されています。

Type::Tinyには、大きく3つの特徴があります。

  • ポータビリティが高い
    • MooseMouseMooなどのクラスビルダの型制約と互換性がある
    • 基本コアモジュールのみに依存している
  • 定義済み型制約ライブラリの使い勝手が良い
  • パフォーマンスが十分良い
    • ピュアPerl実装のType::Tinyでも、Sub::Quote::quote_subで型の判定をインライン展開し、速い
    • Type::Tiny::XSRef::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::LibraryType::UtilsTypes::Standardがあります。Type::Libraryを利用すれば、型制約の再利用が可能になります。Type::Utilsdeclareaswhereenumといった型制約定義のためのDSLDomain Specific Languageドメイン特化言語)を提供します。そしてTypes::Standardは、後述の通りIntStrといった基本的な型制約を提供します。これらのモジュールを利用することで、簡潔に型制約を宣言できます。

次のコードは、偶数と血液型の型制約を定義しています。

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;

型制約EvenBloodは宣言した名前でエクスポートでき、スッキリと記述できます。

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の基本型制約をMooseMouseと同様に提供します。

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

この基本型と組み合わせ、ほかに構造やオブジェクトなどの型制約も利用できます。

以下は、MapDictTupleの例です。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]);

次に、InstanceOfHasMethodsの利用例です。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
    );
}

続いて、EnumOverloadTiedの例です。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;

これで、自前で用意したMyRangeTypes::StandardStrも、MyTypeExtendeduseするだけで利用できます。

use MyTypeExtended -types;

Str->check('foo');
MyRange->check('bar');

型制約のライブラリには、Types::URITypes::Path::TinyTypes::UUIDといった用途がはっきりしたものもあります。たとえばTypes::URIであれば文字列からURIオブジェクトへの暗黙的な変換が行え、わかりきったURIオブジェクトへの変換を省け、型制約に振り回されにくくなります。こういった変換を、型強制と呼びます。

<続きの(3)こちら。>

WEB+DB PRESS

本誌最新号をチェック!
WEB+DB PRESS Vol.130

2022年8月24日発売
B5判/168ページ
定価1,628円
(本体1,480円+税10%)
ISBN978-4-297-13000-8

  • 特集1
    イミュータブルデータモデルで始める
    実践データモデリング

    業務の複雑さをシンプルに表現!
  • 特集2
    いまはじめるFlutter
    iOS/Android両対応アプリを開発してみよう
  • 特集3
    作って学ぶWeb3
    ブロックチェーン、スマートコントラクト、NFT

おすすめ記事

記事・ニュース一覧