本連載では第一線の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)でエラーが発生し中途半端な状態になります。
動的な型制約での解決
この問題を解決するにはどうすればよいでしょうか。
一つの方法は使用する言語を静的言語に変えることです。しかし、これまで開発してきたアプリケーションの言語を変えることは多大な労力が発生しますし、また社内の資産を活かすためPerlを使いたい場合もあるでしょう。
言語を変えずにPerlのまま問題を解決する方法として、動的な型制約の導入があります。動的な型制約では、関数の引数や変数に受け付ける型を制約として記述しておき、プログラムの実行時に制約を満たしているかのチェックを行うことで、関数呼び出し時に制約を満たさなかった場合、関数を実行する前にエラーで終了します。
リスト1の例なら、「func
関数は文字列と数字の2つの引数を必要とする」と制約を記述すれば、(3)や(4)のように間違えた呼び出しをした瞬間にエラーで終了します。そのため呼び出しの間違いでは関数が中途半端に実行されず、データが不整合な状態になりません。
このように実行時に引数の型をチェックすることによって、アプリケーションの堅牢性を高められます。
Smart::Argsで引数に型制約を導入する
それでは、動的な型制約の具体的な導入方法を説明していきましょう。
関数の引数に対して動的な型制約をかけるためのCPANモジュールには、Smart::ArgsやData::Validator、Params::Validateなどがあります。本節では、その中でも実行速度と記法の使いやすさを兼ね備えたSmart::Argsについて解説します。
Smart::Argsは自分で定義した関数の最初に簡潔な記法を記述することで、関数の引数に動的な型制約をかけるモジュールです。このモジュールによって、動的な型制約をかけた関数に間違った引数を渡しても呼び出した瞬間にその場で例外を投げるため、関数が中途半端に実行される問題を防げます。
Smart::Argsは、args
とargs_pos
という2つの記法の使い方と、どんな型を指定できるかだけを覚えれば導入できます。また、関数単位で導入するかどうかを決められるので、新しく書くコードから導入したり、堅牢性を高めたい部分にだけ導入したりと気軽に使い始められます。
Smart::Argsのインストール
Smart::ArgsはCPANモジュールとして提供されているので、cpanm
コマンドでインストールします。モジュールをロードできれば、正しくインストールできています。
Smart::Argsの基本的な使い方
さっそくSmart::Argsを使って、関数に動的な型制約を付けてみましょう。ここでは名前付き引数をチェックするargs
記法や、その記法に渡すことができるオプション、名前付き引数ではなく通常の引数で受け取るためのargs_pos
記法などを紹介します。
args──名前付き引数をチェックする
まずargs
という記法を使い、関数の名前付き引数に対して動的な型制約をかけてみます。名前付き引数とは、func(number => 1, string => 'abc')
のように、引数にnumber
やstring
のような名前を付けて渡すものです。
たとえばfunc
という関数があり、引数としてnumber
という名前で整数を、string
という名前で文字列を受け取りたいとします。そのように型制約を付けたい場合、次のように関数を定義し、args
記法で制約を書きます。
定義したfunc
関数を正しい引数で呼び出すと、number
という名前で渡した引数は$number
に、string
という名前で渡した引数は$string
に/代入され、関数内で利用できます。
もし関数の使い方を間違えて、渡した引数の型が制約を満たさない場合、Smart::Argsはその関数を実行せずに例外を出します。どう間違えているかは標準エラー出力にエラーメッセージとして表示されます。たとえば次のコードは、func
関数のnumber
という引数に、間違えて整数ではなく小数1.5を渡したものです。
この場合、'number': Validation failed for 'Int'with value 1.5
のように、「number
という引数はInt
を期待していたが1.5という値が指定されている」というエラーを出して、func
関数を実行せずに終了します。
Smart::Argsで型制約をかけた場合、デフォルトでは定義した引数が渡されないときもエラーを発生させます。次のコードは、string
という名前の引数を渡し忘れた例です。
この場合も、missing mandatory parameter named'$string'
のように、「string
という引数は必須だが渡されていない」というエラーを出して、func
関数を実行せずに終了します。
args
記法に渡せる型には、Int
やStr
などさまざまなものがあります。指定できる型について詳しくは、「制約として指定できる型」で後述します。
default──引数を渡さない場合のデフォルト値を設定する
先ほど、Smart::Argsでは引数を渡さなかった場合もエラーを発生させると紹介しました。しかし型制約の定義時にオプションを利用すれば、渡さなかった場合のデフォルト値を設定できます。
オプションを利用したい場合は、型制約としてInt
などの文字列を渡していた部分にハッシュを渡します。型はisa
に記述し、デフォルト値はdefault
に記述します。
次のコードは、p
という引数を渡さなかった場合、デフォルト値として10を利用する例です。
クラスメソッド、インスタンスメソッドに型制約を付ける
今までは関数に型制約をかけてきましたが、クラスメソッドやインスタンスメソッドに型制約を付けることもできます。
リスト2は、Foo
パッケージにクラスメソッドとインスタンスメソッドを定義した例です。メソッドに型制約を導入する場合は、(1)や(2)のように、1つ目の型制約を$class
もしくは$self
という変数名を使って定義します。このようにすることで、$class
や$self
の変数に、クラスメソッドとして呼び出した場合はクラス名が、インスタンスメソッドとして呼び出した場合はそのオブジェクトが代入されます。また、クラス名であるという型制約にはClassName
を使えます。
args_pos──通常の引数をチェックする
args
では名前付き引数を取り扱いました。しかし名前付き引数ではなく、もっとシンプルにfunc(3,'abc')
のように通常の引数として受け取りたい場合もあります。このときはargs_pos
を利用します。記法の名称が変わっただけで、使い方はargs
と同じです。
制約として指定できる型
ここでは、型制約として指定できる型について紹介します。
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
クラスのインスタンスを受け取る関数を定義する例です。
ほかにもMouse::Util::TypeConstraintsというモジュールを利用すれば、独自に型を定義できます。独自の型の定義方法は次節で紹介します。
ここまでで紹介したさまざまな型を利用すれば、URIクラスのオブジェクトの配列もしくはundef
という型を定義できます。
<続きの(2)はこちら。>
- 特集1
イミュータブルデータモデルで始める
実践データモデリング
業務の複雑さをシンプルに表現!
- 特集2
いまはじめるFlutter
iOS/Android両対応アプリを開発してみよう
- 特集3
作って学ぶWeb3
ブロックチェーン、スマートコントラクト、NFT