Perl Hackers Hub

第40回Perl開発への動的な型制約の導入(1)

本連載では第一線のPerlハッカーが回替わりで執筆していきます。今回のハッカーは、はてなでPerlを利用したWebアプリケーションを開発している@shiba_yu36こと柴崎優季さんで、テーマは「Perl開発への動的な型制約の導入」です。

本稿のサンプルコードは、WEB+DB PRESS Vol.94のサポートサイトから入手できます。

なぜ動的な型制約を導入したいのか

動的な型制約とは、関数の引数や変数に受け付ける型を制約として記述しておき、プログラムの実行時に制約を満たしているかのチェックを行うしくみのことです。静的な言語ではコンパイル時に型チェックを行っているのですが、それを動的な言語で実行時に行います。

動的な型制約をPerlへ導入する方法の解説に入る前に、本節でまず、なぜPerlに動的な型制約を導入したいのかについて説明します。

型がないことによる問題

Perlを使うメリットにはたとえば次のものがあります。

  • 枯れている言語でドキュメントも多く、安定して利用できる
  • CPANモジュールにたくさんの資産があり、かつそれらのドキュメントは同じような形式で書かれていて扱いやすい
  • さまざまな環境にデフォルトで実行環境が入っている
  • 動的言語でコンパイルがなく、型を意識する必要がないため、学習コストが比較的小さく、すばやく開発できる

しかし、メリットの一つである型を意識する必要がないことは、問題もあります。問題の一つは、関数の呼び出し方を間違えても気付けないことです。動的言語では、関数の呼び出しで次のどちらかの間違いを犯しても、エラーが起こるまでその関数の実行を進めてしまいます。

  • 2つの引数を必要としている関数に1つしか引数を渡さなかった
  • Blogクラスのオブジェクトを必要としているところに、別のクラスのオブジェクトを渡した

エラーが起こるまで実行を進めることは、最悪の場合、扱うデータを不整合な状態にする可能性があります。

リスト1は、関数呼び出しを間違えてデータが不整合な状態になる例です。funcは1つ目の引数に文字列を、2つ目の引数に数字を受け取り、処理を行う関数です。func内では最初に(1)$stringを使った処理を、次に(2)$numberを使った処理を行います。しかし、(3)の呼び出しでは2つ目の引数を渡し忘れています。このとき(1)の処理は実行されますが、(2)の処理でエラーが発生し実行が終了します。もし(1)の部分でファイルの変更やデータベースの更新などの副作用のある処理をしていたら、関数は適切に動作を終えられず、データは不整合な状態になってしまいます。また、(4)のように数字を期待しているところに文字を渡しても、(2)でエラーが発生し中途半端な状態になります。

リスト1 関数の間違った呼び出しによる不整合の発生
sub func {
    my ($string, $number) = @_;
    // (1)$stringを使った処理
    ...
    // (2)$numberを使った処理
    ...
}

// (3)引数を1つしか渡していない
func('aiueo');

// (4)2つ目の引数に数字以外を渡す
func('aiueo', 'abcde');

動的な型制約での解決

この問題を解決するにはどうすればよいでしょうか。

一つの方法は使用する言語を静的言語に変えることです。しかし、これまで開発してきたアプリケーションの言語を変えることは多大な労力が発生しますし、また社内の資産を活かすためPerlを使いたい場合もあるでしょう。

言語を変えずにPerlのまま問題を解決する方法として、動的な型制約の導入があります。動的な型制約では、関数の引数や変数に受け付ける型を制約として記述しておき、プログラムの実行時に制約を満たしているかのチェックを行うことで、関数呼び出し時に制約を満たさなかった場合、関数を実行する前にエラーで終了します。

リスト1の例なら、func関数は文字列と数字の2つの引数を必要とする」と制約を記述すれば、(3)(4)のように間違えた呼び出しをした瞬間にエラーで終了します。そのため呼び出しの間違いでは関数が中途半端に実行されず、データが不整合な状態になりません。

このように実行時に引数の型をチェックすることによって、アプリケーションの堅牢性を高められます。

Smart::Argsで引数に型制約を導入する

それでは、動的な型制約の具体的な導入方法を説明していきましょう。

関数の引数に対して動的な型制約をかけるためのCPANモジュールには、Smart::ArgsやData::Validator、Params::Validateなどがあります。本節では、その中でも実行速度と記法の使いやすさを兼ね備えたSmart::Argsについて解説します。

Smart::Argsは自分で定義した関数の最初に簡潔な記法を記述することで、関数の引数に動的な型制約をかけるモジュールです。このモジュールによって、動的な型制約をかけた関数に間違った引数を渡しても呼び出した瞬間にその場で例外を投げるため、関数が中途半端に実行される問題を防げます。

Smart::Argsは、argsargs_posという2つの記法の使い方と、どんな型を指定できるかだけを覚えれば導入できます。また、関数単位で導入するかどうかを決められるので、新しく書くコードから導入したり、堅牢性を高めたい部分にだけ導入したりと気軽に使い始められます。

Smart::Argsのインストール

Smart::ArgsはCPANモジュールとして提供されているので、cpanmコマンドでインストールします。モジュールをロードできれば、正しくインストールできています。

$ cpanm Smart::Args
$ perl -MSmart::Args -E 'say $Smart::Args::VERSION'

Smart::Argsの基本的な使い方

さっそくSmart::Argsを使って、関数に動的な型制約を付けてみましょう。ここでは名前付き引数をチェックするargs記法や、その記法に渡すことができるオプション、名前付き引数ではなく通常の引数で受け取るためのargs_pos記法などを紹介します。

args─⁠─名前付き引数をチェックする

まずargsという記法を使い、関数の名前付き引数に対して動的な型制約をかけてみます。名前付き引数とは、func(number => 1, string => 'abc')のように、引数にnumberstringのような名前を付けて渡すものです。

たとえばfuncという関数があり、引数としてnumberという名前で整数を、stringという名前で文字列を受け取りたいとします。そのように型制約を付けたい場合、次のように関数を定義し、args記法で制約を書きます。

use Smart::Args;

sub func {
    args my $number => 'Int',
         my $string => 'Str';

    print(sprintf('%d:%s', $number, $string));
}

定義したfunc関数を正しい引数で呼び出すと、numberという名前で渡した引数は$numberに、stringという名前で渡した引数は$stringに/代入され、関数内で利用できます。

func(number => 10, string => 'aiueo');
# 10:aiueo と出力される

もし関数の使い方を間違えて、渡した引数の型が制約を満たさない場合、Smart::Argsはその関数を実行せずに例外を出します。どう間違えているかは標準エラー出力にエラーメッセージとして表示されます。たとえば次のコードは、func関数のnumberという引数に、間違えて整数ではなく小数1.5を渡したものです。

func(number => 1.5, string => 'aiueo');

この場合、'number': Validation failed for 'Int'with value 1.5のように、numberという引数はIntを期待していたが1.5という値が指定されている」というエラーを出して、func関数を実行せずに終了します。

Smart::Argsで型制約をかけた場合、デフォルトでは定義した引数が渡されないときもエラーを発生させます。次のコードは、stringという名前の引数を渡し忘れた例です。

func(number => 10);

この場合も、missing mandatory parameter named'$string'のように、stringという引数は必須だが渡されていない」というエラーを出して、func関数を実行せずに終了します。

args記法に渡せる型には、IntStrなどさまざまなものがあります。指定できる型について詳しくは、⁠制約として指定できる型」で後述します。

default─⁠─引数を渡さない場合のデフォルト値を設定する

先ほど、Smart::Argsでは引数を渡さなかった場合もエラーを発生させると紹介しました。しかし型制約の定義時にオプションを利用すれば、渡さなかった場合のデフォルト値を設定できます。

オプションを利用したい場合は、型制約としてIntなどの文字列を渡していた部分にハッシュを渡します。型はisaに記述し、デフォルト値はdefaultに記述します。

次のコードは、pという引数を渡さなかった場合、デフォルト値として10を利用する例です。

use Smart::Args;
sub func_with_default {
    args my $p => { isa => 'Int', default => 10 };
    print($p);
}

func_with_default(p => 5);
# 5と出力される
func_with_default();
# 10と出力される

クラスメソッド、インスタンスメソッドに型制約を付ける

今までは関数に型制約をかけてきましたが、クラスメソッドやインスタンスメソッドに型制約を付けることもできます。

リスト2は、Fooパッケージにクラスメソッドとインスタンスメソッドを定義した例です。メソッドに型制約を導入する場合は、(1)(2)のように、1つ目の型制約を$classもしくは$selfという変数名を使って定義します。このようにすることで、$class$selfの変数に、クラスメソッドとして呼び出した場合はクラス名が、インスタンスメソッドとして呼び出した場合はそのオブジェクトが代入されます。また、クラス名であるという型制約にはClassNameを使えます。

リスト2 メソッドに型制約を付ける
package Foo;
use Smart::Args;

sub new {
    my ($class) = @_;
    return bless {}, $class;
}

sub class_method {
    args my $class => 'ClassName', ━(1)
         my $p => 'Int';
}

sub instance_method {
    args my $self, ━(2)
         my $q => 'Str';
}

Foo->class_method(p => 3);
my $foo = Foo->new;
$foo->instance_method(q => 'abc');

args_pos─⁠─通常の引数をチェックする

argsでは名前付き引数を取り扱いました。しかし名前付き引数ではなく、もっとシンプルにfunc(3,'abc')のように通常の引数として受け取りたい場合もあります。このときはargs_posを利用します。記法の名称が変わっただけで、使い方はargsと同じです。

use Smart::Args;

sub func_with_args_pos {
    args_pos my $p => 'Int',
             my $q => { isa => 'Str', default => 'aiu' };
    print(sprintf('%d:%s', $p, $q));
}

func_with_args_pos(3, 'abc');
# 3:abcと出力される

制約として指定できる型

ここでは、型制約として指定できる型について紹介します。

Smart::Argsは内部でCPANモジュールのMouseの型のしくみを利用しており、次のものを型として指定できます。

  • Mouseからデフォルトで提供されている型
  • クラス名
  • Mouse::Util::TypeConstraintsを利用して、自分で独自に定義した型

Mouseからデフォルトで提供されている代表的な型には、表1のものがあります。それ以外のデフォルトで提供されている型についてはMouse::Util::TypeConstraintsのドキュメント「Default Type Constraints」を参照してください。

表1 代表的な型とその意味
型名意味
Bool真偽値を受け付ける型
Int整数を受け付ける型
Num小数も含めた数字を受け付ける型
Str文字列を受け付ける型。ただし、Perlでは整数や小数も文字列として受け付けるので注意
ArrayRef[type]配列リファレンスを受け付ける型。typeに型を指定すると、type型の配列リファレンスという制約をかけられる。たとえばArrayRef[Int]とすれば整数の配列を受け付ける
HashRef[type] ハシュリファレンスを受け付ける型。typeに型を指定すると、そのハッシュリファレンスのvalueの型に制約をかけられる
Maybe[type]typeとして指定した型、もしくはundefを受け付ける型。Maybe[Int]とすれば、整数もしくはundefを受け付ける

クラス名を型として指定すると、そのクラスのオブジェクトおよび、そのクラスを継承したクラスのオブジェクトを渡すことができます。型制約としてURIと指定すると、CPANモジュールとして提供されているURIクラスのインスタンスを渡せます。また、前述したリスト2のように自分でクラスを作った場合も、そのクラス名を型として利用できます。次のコードは、引数としてuriという名前でURIクラスのインスタンスを、fooという名前でFooクラスのインスタンスを受け取る関数を定義する例です。

use URI;
use Smart::Args;
sub args_with_uri_and_foo {
    args my $uri => 'URI',
         my $foo => 'Foo';
}

my $uri = URI->new('http://example.com/');
my $foo = Foo->new;
args_with_uri_and_foo(uri => $uri, foo => $foo);

ほかにもMouse::Util::TypeConstraintsというモジュールを利用すれば、独自に型を定義できます。独自の型の定義方法は次節で紹介します。

ここまでで紹介したさまざまな型を利用すれば、URIクラスのオブジェクトの配列もしくはundefという型を定義できます。

sub args_with_uri_array {
    args my $uris => 'Maybe[ArrayRef[URI]]';
}

my $uri1 = URI->new;
my $uri2 = URI->new;
# 呼び出しは両方成功する
args_with_uri_array(uris => [ $uri1, $uri2 ]);
args_with_uri_array(uris => undef);

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

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

おすすめ記事

記事・ニュース一覧