Mobile Factory Tech Blog

技術好きな方へ!モバイルファクトリーのエンジニアたちが楽しい技術話をお届けします!

aliasedでFunction::Parametersの型指定を簡潔にする

 この記事はモバイルファクトリー 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関数を用意し、型指定に使う

metacpan.org

 このモジュールは、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の値をクラス名とみなすか値とするか区別する文法は型制約依存無しに実現するのは難しいように思います。こういった理由から、現在の仕様で良いと自分は思っています。