この記事はモバイルファクトリー Advent Calendar 2019 2日目の記事です。
ヒューマンリレーションズ部シニアエンジニアのid:kfly8です。昨日、id:karupaneruraが公開していた2019年の最先端のPerl開発ボイラープレートにて、
Function::Parametersで (InstanceOf['Point']) と括弧でくくる必要がある
とありました。この記事ではこの補足をしたいと思います。
まず、問題の整理をしたいと思います。今回、2つの観点で、期待と異なっていると思います。まず、InstanceOf['Point']
は、Type::Tinyの式としては正しいので、そのまま、Function::Parametersでも使わせて欲しいはずです。
# Type::Tinyの式としては正しい use Types::Standard -types; my $type = InstanceOf['Point']; # けれど、Function::Parametersではそのまま使えない fun foo(InstanceOf['Point'] $foo) { ... } # error!
もう一つ期待することは、Function::Parametersのissueにあるように、クラス名を型制約として利用したいはずです。普段、Mouseに慣れていると、isaにクラス名を文字列で指定できるので尚更です。
# Mouseを使っていれば、クラス名を文字列で指定できる use Mouse; has foo => ( is => 'ro', isa => 'Foo', # Fooオブジェクトを期待 );
今回この期待通りではないので、違和感を感じさせていると思います。
Function::Parameters の型指定の仕様
上述の問題は、一言でまとめてしまうと、Function::Parametersの仕様のためですが、もう少し納得感がでるように、Function::Parametersの仕様を掘り下げてみたいと思います。Function::Parametersのドキュメントにも同様の説明があるので、合わせて確認してみて欲しいです。
まず一番簡単な例を説明します。Function::Parametersは、Str
と型指定した場合はStr
関数が同一パッケージに定義されているかコンパイル時に探索します。Str
関数が定義されていなければ、コンパイル時にエラーになります。
# Strと指定することで、Str関数が定義されているかコンパイル時に探索する fun foo(Str $s) { ... }
パラメタ付きの型ArrayRef[Int]
を指定した場合はInt
関数とArrayRef
関数が定義済みかコンパイル時に確認し、Str | Undef
のpipeでUnion型とした場合はStr
関数とUndef
関数が定義済みかコンパイル時に確認します。
# IntとArrayRef関数が定義されているか探索 fun bar(ArrayRef[Int] $numbers) { ... } # Str, Unde関数が定義されているか探索 fun baz(Str|Undef $maybe_str) { ... }
つまり、Function::Parametersの型指定の文法仕様は、識別子と同名の関数を結びつける仕様になっています。
- Function::Parametersの型指定の仕様
- 識別子(例:
Str
) - パラメタ付き識別子(例:
ArrayRef[...]
) - 識別子の合成(例:
Str | Undef
)
- 識別子(例:
なので、InstanceOf['Int']
のように型指定をした場合、型定義が見つけられず、文法エラーになります。
fun fuga(InstanceOf['Foo'] $a) { ... } # => In fun fuga: missing type name after '['
さらに、この仕様により、Strr
のように指定する型名を間違えた場合は、コンパイル時にエラーにできます。つまり、型定義されていなければ、コンパイル時にエラーになると考え、開発できます。 これは他のPerlのValidationモジュールにないメリットだと思います。
fun foo(Strr $a) { ... } # => Undefined type name main::Strr
どうしても逃げ道が欲しい場合、式を()
で包めば、式を評価する仕様があるのでそれを使います。例えば(InstanceOf['Foo'])
と型指定した場合は、括弧内のInstanceOf['Foo']
という式をコンパイル時に評価します。
# InstanceOf['Foo'] を括弧で包むと、括弧内を式として評価される fun fuga((InstanceOf['Foo']) $a) { ... }
ただ、この書き方のデメリットはあります。まず、式を評価しているだけなので、エラーメッセージから原因が推測しにくいです。
fun fuga((IstanceOf['Foo']) $a) { ... } # XXX: InstanceOf のつづりが間違っている # => syntax error # これでは、エラーの原因が分かりにくい
また、クラス名を間違えても、文法エラーになりません。
# クラス名を間違えても文法エラーにならない fun fuga((InstanceOf['Foooo']) $a) { ... } # => syntax OK
もし識別子の書き方に統一できれば、コンパイル時に問題に気づけます。 式表現は柔軟ではありますが、極力、使わないようにした方が良いと思います。その方法について次の段落に書きます。
aliasedで文字列のalias関数を用意し、型指定に使う
このモジュールは、Too::Long::Fooというクラスをロードしつつ、Fooといった名前で利用できるようにするモジュールです。SYNOPSISは次の通りです。
use aliased 'Too::Long::Foo'; my $foo = Foo->new; print Foo; # => Too::Long::Foo
aliasedを利用することで、Too::Long::Fooというクラスを、(InstanceOf['Too::Long::Foo'])
といった記述をすることなく、InstanceOf[Foo]
といった形で書けます。Fooが関数になったので、こういった記述ができます。簡潔ですね!
use Types::Standard -types; use Function::Parameters; use aliased "Too::Long::Foo"; fun foo(InstanceOf[Foo] $foo ) { ... } # これは、fun foo( (InstanceOf['Too::Long::Foo']) $foo) { ... } と同じ
また、aliasedを使うことで、useできたクラスしか関数として使えないので、Foooo
といったタイポにもコンパイル時に気づけます。
use aliased "Too::Long::Foo"; fun foo(InstanceOf[Foooo] $foo) { ... } # => Foooo が見つからないので、コンパイル時にエラー
- aliasedとFunction::Parametersを組み合わせることで得られるメリット
- 簡潔
- 型の指定方法が、識別子に統一できる
()
のあるなしで迷わない- 識別子か文字列か迷わない
- クラス名の名前を間違えても、コンパイル時に気づける
まとめ
- Function::Parametersの型指定は、識別子と同名の関数が定義済か探索する
- 型指定に、式表現を用いることができ、柔軟だが、エラーがわかりにくい
- aliasedを使えば、文字列を関数にでき、識別子を使った型指定方法に寄せられ、簡潔に記述でき、エラーも検知しやすい
まとめるとこんな感じです!
おまけ
そもそも、なぜFunction::Parametersが識別子を基本とする仕様にしているのか考えてみます。上述の通り、簡潔、エラーが検知しやすいことは理由になると思います。自分が考えるもう一つの理由は、Function::Parametersが好きな型制約を使えるようにする為です。Function::Parametersが型制約に要求することは、check、get_messageが型制約にduck typeされていることだけです。これにより、Type::Tiny、Mouse::Meta::TypeConstraintといった複数の型制約クラスを使えます。もしクラス名を型制約に変換するとしたら、Data::Validatorであれば、Mouseに依存させていますが、Function::Parametersでは、特定の型制約に依存させないといけないかもしれません。これは大きなトレードオフだと思います。さらに、Enum["Foo", "Bar"]
のようにEnumの値をクラス名とみなすか値とするか区別する文法は型制約依存無しに実現するのは難しいように思います。こういった理由から、現在の仕様で良いと自分は思っています。