Blog::kobaken

prove t/foo/bar/baz.t

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:内部的には、パフォーマンス、互換性、スコープ拡大、型表現力強化ののために様々なことをしている巨大モジュールになっていますが