Moose-0.92 > Moose::Manual::Types

題名

Moose::Manual::Types - Mooseの型システム

Perlで型?

Mooseはアトリビュートのために独自の型システムを用意しています。また、MooseXモジュールの助けを借りるとこれらの型をメソッドのパラメータの検証に使うこともできます。

Mooseの型システムのもとになったのは、Perl 5自身の「暗黙の」型と、Perl 6のいくつかのコンセプトを組み合わせたものです。独自の制約を使うと自前のサブタイプを簡単に作れるため、どのような種類の検証コードでも簡単に表現できます。

型には名前がついているので、名指しで再利用できます。そのため、大きなアプリケーション全体で型を共有することも簡単にできます。

ただし、ここで明確にしておきたいのは、これは「本当の」型システムではないということ。Mooseを使うと魔法の力でPerlが型と変数を関連づけるようになるわけではありません。これは単に名前と制約を関連づけられる高度なパラメータチェックシステムにすぎないのです。

とはいえ、これは本当に便利なものですし、Mooseを楽しく、強力にしている機能のひとつだと思っています。型システムを活用すると、確実に有効なデータを得られるようにするのがはるかに簡単になりますし、コードの保守性にも大きく貢献します。

基本的なMooseの型階層はこのようになります。

  Any
  Item
      Bool
      Maybe[`a]
      Undef
      Defined
          Value
              Num
                Int
              Str
                ClassName
                RoleName
          Ref
              ScalarRef
              ArrayRef[`a]
              HashRef[`a]
              CodeRef
              RegexpRef
              GlobRef
                FileHandle
              Object
                Role

実用的には、AnyItemの違いは概念的なものでしかありません(Itemは型階層の最上位の型として扱われます)。

それ以外の型は既存のPerlの概念に対応しています。たとえば、NumはPerlが数字だとみなすものすべてであり、Objectはblessされたリファレンスである、という具合です。

型のあとに「[`a]」と書いてあるのはパラメータを指定できるものです。だから、単に素のArrayRefがほしいと書くかわりに、ArrayRef[Int]がほしいと書けるわけです(それどころか、HashRef[ArrayRef[Str]]のようなこともできます)。

特筆に値するのはMaybe[`a]という型です。単独で使うと本当に何の意味もないのですが(Itemと同等になります)、パラメータを指定すると、undefかパラメータで指定した型の値をとる、という意味になります(だから、Maybe[Int]であれば整数かundefになります)。

型階層の詳細についてはMoose::Util::TypeConstraintsをご覧ください。

型とはなにか

大事なのは、型はクラス(やパッケージ)ではないということです。型は名前と制約を持った単なるオブジェクト(正確にいうならMoose::Meta::TypeConstraintのオブジェクト)にすぎません。Mooseは、Numのような名前を適切なオブジェクトに変換できるように、グローバルな型レジストリを持っています。

ただし、クラス名が型の名前になることは「ありえます」。Mooseを使って新しいクラスを定義すると、裏ではそのクラスに関連づけられた型の名前が定義されます。

  package MyApp::User;

  use Moose;

こうすると、'MyApp::User'が型の名前として使えるようになります。

  has creator => (
      is  => 'ro',
      isa => 'MyApp::User',
  );

ただし、Mooseを使っていないクラスの場合、魔法がきかないので、クラス型を明示的に宣言する必要があるかもしれません。いささかややこしいことに、Mooseはアトリビュートのisaの値として未知の型の名前が渡されるとクラスであると仮定するので、これはうまくいきます。

  has 'birth_date' => (
      is  => 'ro',
      isa => 'DateTime',
  );

一般的に、Mooseは未知の名前を渡すとクラスを指しているものと仮定します。

  subtype 'ModernDateTime'
      => as 'DateTime'
      => where { $_->year() >= 1980 }
      => message { 'The date you provided is not modern enough' };

  has 'valid_dates' => (
      is  => 'ro',
      isa => 'ArrayRef[DateTime]',
  );

だから、どちらの場合も、DateTimeはクラス名であるとみなされます。

サブタイプ

サブタイプは、Mooseの組み込みの階層でも利用されています(たとえば、IntNumの子です)。

サブタイプは親の型や制約によって定義されます。親が定義している制約があれば先にそれがチェックされ、それからサブタイプが定義している制約がチェックされます。これらのチェックを「すべて」満たさないと、そのサブタイプの有効な値とはみなされません。

典型的なサブタイプは、親の制約を受け継いでより具体的なものにします。

また、サブタイプには制約を満たさなかったときのために独自のメッセージを定義できます。これを使うと「入力された値(20)は有効な評価ではありません。1~10までの数を入れてください」といったエラーを表示させられます(これは、その値は型の検証チェックに失敗しましたとしか言ってくれないデフォルトのエラーよりはるかに親切です)。

これは簡単な(しかも役に立つ)サブタイプの例です。

  subtype 'PositiveInt'
      => as 'Int'
      => where { $_ > 0 }
      => message { "The number you provided, $_, was not a positive number" }

なお、型を扱うためのシュガー関数はすべてMoose::Util::TypeConstraintsがエクスポートしています。

(サブタイプではない)新しい型を作る

新たに最上位の型を作ることもできます。

  type 'FourCharacters' => where { defined $_ && length $_ == 4 };

実用上、これはStrをサブタイプ化するのと大差ありません(ただし、値が定義されているかどうかは自分でチェックしなければなりません)。

DefinedRefObjectのように非常に大まかな型をサブタイプ化するだけではすまない場合があるとはあまり考えられません。

新たに最上位の型を定義するのは、概念的にはItemをサブタイプ化するのと同じことです。

型の名前

型の名前は、現在実行中のPerlインタプリタ全体に影響を及ぼすグローバルなものです。内部的には、Mooseはレジストリを通じて型の名前を型オブジェクトにマッピングします。

同じプロセス内にMooseを使っているアプリケーションやライブラリが複数ある場合、名前の衝突による問題が起こるかもしれません。この種の衝突を防ぐために、型の名前には何らかの名前空間を示すプレフィックスをつけることをおすすめします。

たとえば、型に「PositiveInt」という名前をつけるかわりに「MyApp::Type::PositiveInt」や「MyApp::Types::PositiveInt」と名付けてください。このような型の定義はすべてMyApp::Typesというひとつのパッケージにまとめて、アプリケーションのほかのクラスからもロードできるようにすることをおすすめします。

型変換

Mooseの型システムでもっとも強力な機能のひとつが、型変換です。型変換は、ある型を別の型に変える方法のひとつです。

  subtype 'ArrayRefOfInts'
      => as 'ArrayRef[Int]';

  coerce 'ArrayRefOfInts'
      => from 'Int'
      => via { [ $_ ] };

お気づきの通り、ここはArrayRef[Int]を直接型変換するのではなく、サブタイプを作らなければならないところでした(この辺はちょっとわかりづらいところです)。

型変換も、型の名前と同じくグローバルなものです(これが型の名前に名前空間をつけた方がよい「もうひとつの」理由です)。Mooseは、明示的に指示しない限り「決して」値を型変換しようとはしません。型変換を指示するには、coerceというアトリビュートオプションを真値にセットします。

  package Foo;

  has 'sizes' => (
      is     => 'ro',
      isa    => 'ArrayRefOfInts',
      coerce => 1,
  );

  Foo->new( sizes => 42 );

このサンプルコードは正しく動作します。新しく作ったオブジェクトのsizesアトリビュートの値は[ 42 ]になります。

再帰的な型変換

再帰的な型変換というのは、パラメータ付きの型の型パラメータを型変換することです。このような型を例に取ってみましょう。

  subtype 'HexNum'
      => as 'Str'
      => where { /[a-f0-9]/i };

  coerce 'Int'
      => from 'HexNum'
      => via { hex $_ };

  has 'sizes' => (
      is     => 'ro',
      isa    => 'ArrayRef[Int]',
      coerce => 1,
  );

sizesアトリビュートに16進数の配列リファレンスを渡そうとしても、Mooseは型変換してくれません。

ただし、サブタイプをひと組定義すると、パラメータ付きの型同士の型変換を有効にすることができます。

  subtype 'ArrayRefOfHexNums'
      => as 'ArrayRef[HexNum]';

  subtype 'ArrayRefOfInts'
      => as 'ArrayRef[Int]';

  coerce 'ArrayRefOfInts'
      => from 'ArrayRefOfHexNums'
      => via { [ map { hex } @{$_} ] };

  Foo->new( sizes => [ 'a1', 'ff', '22' ] );

これでMooseは16進数を整数に型変換してくれるようになりました。

ただし、Mooseは型変換を連鎖的に実行してくれるわけではないので、このままでは単独の16進数の型変換はしてくれません。これをさせるには、別の型変換を定義する必要があります。

  coerce 'ArrayRefOfInts'
      => from 'HexNum'
      => via { [ hex $_ ] };

たしかにこれは非常に冗長になってしまうこともありますが、型変換はトリッキーな魔法ですから、明示的にしておくのがいちばんだと思っています。

型結合

Mooseを使うと、複数の異なる型になれるアトリビュートを定義することができます。たとえば、ここではObjectFileHandleなら認めてもよい、というわけです。

  has 'output' => (
      is  => 'rw',
      isa => 'Object | FileHandle',
  );

Mooseは実際にその文字列を解析して、型結合を作っていることを認識すると、outputアトリビュートにはあらゆる種類のオブジェクトと、blessされていないファイルハンドルを受け入れるようにさせます。自分のコードの中でそのそれぞれについて正しい処理を行うのはみなさんの仕事です。

型結合を使う場合はかならず型変換の方がよい解決策ではないか検討するようにしてください。

上の例の場合、もっと具体的に、outputはprintメソッドを持つオブジェクトでなければならないと定義した方がよいかもしれません。

  subtype 'CanPrint'
      => as 'Object'
      => where { $_->can('print') };

簡単なラッパクラスを使えば、ファイルハンドルをこの条件を満たすオブジェクトに型変換することもできます。

  package FHWrapper;

  use Moose;

  has 'handle' => (
      is  => 'rw',
      isa => 'FileHandle',
  );

  sub print {
      my $self = shift;
      my $fh   = $self->handle();

      print $fh @_;
  }

これでFileHandleからこのラッパクラスへの型変換を定義できるようになりました。

  coerce 'CanPrint'
      => from 'FileHandle'
      => via { FHWrapper->new( handle => $_ ) };

  has 'output' => (
      is     => 'rw',
      isa    => 'CanPrint',
      coerce => 1,
  );

このように型結合のかわりに型変換を使うと、クラスの内部をよりシンプルにできるようになります。

型を生成するためのヘルパー関数

Moose::Util::TypeConstraintsモジュールは、class_typerole_typemaybe_typeのように、特定の種類の型を生成するためのヘルパー関数を多数エクスポートします。詳しくはドキュメントをご覧ください。

特筆に値するヘルパーとしては、enumがあります。これを使うと、指定した値しか許さないStr型のサブタイプを生成できます。

  enum 'RGB' => qw( red green blue );

これでRGBという名前の型が生成されます。

無名の型

型を生成する関数はすべて型オブジェクトを返します。この型オブジェクトは、親の型や、アトリビュートのisaオプションの値のように、型の名前を使える場所ならどこででも使えます。

  has 'size' => (
      is => 'ro',
      isa => subtype 'Int' => where { $_ > 0 },
  );

これは、その場限りの型を作る(グローバルな名前空間レジストリを「汚染」したくない)ときに便利です。

検証メソッドのパラメータ

Mooseには検証メソッドにパラメータを渡す手段はありませんが、CPANにはこれをできるようにするMooseX拡張モジュールがいくつかあります。

もっとも簡単で、甘さ控えめなのはMooseX::Params::Validateです。このモジュールを使うと、名前付きパラメータの組を検証するときにMooseの型を使えます。

  use Moose;
  use MooseX::Params::Validate;

  sub foo {
      my $self   = shift;
      my %params = validated_hash(
          \@_,
          bar => { isa => 'Str', default => 'Moose' },
      );
      ...
  }

MooseX::Params::Validateは型変換もサポートしています。

Mooseの型を使ったメソッドパラメータの検証をサポートしている拡張モジュールの中には、ほかにももっと強力ながいくつかあります。そのひとつであるMooseX::Method::Signaturesは、本格的なmethodキーワードを提供してくれます。

  method morning (Str $name) {
      $self->say("Good morning ${name}!");
  }

ロード順の問題

Mooseの型は実行時に定義されるので、ロード順の問題に遭遇することがあります(特に、定義される前にクラス型の制約を使おうとしてしまうことがあります)。

この問題にはいくつかおすすめの解決策があります。まず、独自の型は「すべて」MyApp::Typesというモジュールに定義してください。次に、このモジュールを、ほかのすべてのモジュールでロードするようにしてください。

それでもまだロード順の問題が起こる場合は、Moose::Util::TypeConstraintsがエクスポートしているfind_type_constraintを使う手もあります。

  class_type('MyApp::User')
      unless find_type_constraint('MyApp::User');

このような「なければ作る」というロジックは、書くのも簡単ですし、ロード順の問題の対策にもなります。

作者

Dave Rolsky <autarch@urth.org>

コピーライト & ライセンス

Copyright 2009 by Infinity Interactive, Inc.

http://www.iinteractive.com

This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself.