Perl Hackers Hub

第52回 Perlで堅牢な開発―構文チェック,静的検査,型制約(3)

この記事を読むのに必要な時間:およそ 3 分

(1)こちら⁠2)こちらから。

関数に型制約を導入する

関数の引数と戻り値に対し型制約を適用することで,期待する値を限定でき堅牢さが高まります。

Function::Parameters─⁠─関数の引数の型制約

Type::TinyにはType::Paramsという引数チェック用ライブラリが同梱されていますが,本稿ではFunction::Parameters 2.001003を解説します。Function::Parametersはパフォーマンスこそ若干Type::Paramsに劣るものの注1)⁠後述するコード検査が行える利点があります。

注1)
参考までにベンチマークスクリプトをサポートサイトに置いているので,興味がある人はご確認ください。
Function::Parametersの使い方

Function::Parametersは,funもしくはmethodキーワードで関数定義を行います。関数の引数にStr $msgと書けば,$msgStrの型制約を満たすかを判定します。指定する型制約は,値が期待どおりか判定するcheckメソッドと,エラーメッセージを返すget_messageメソッドさえダックタイピングされていればよく,Type::Tinyはもちろん,Moose(あるいはMouse)::Meta::TypeConstraintも利用できます。

use Function::Parameters;
use Types::Standard -types;

hello('world!'); # => HELLO world!

fun hello(Str $msg) {
    print "HELLO $msg";
}

名前付き引数は:$nameのように変数名にコロンを付け,初期値は$name=DEFAULTのように指定できます。

foo(name => 'foo!'); # => foo!

fun foo(Str :$name) {
    print $name;
}

bar(); # => bar!

fun bar(Str $name='bar!') {
    print $name
}
Function::Parametersでコード検査を行う

関数の引数が多すぎると誤用が起きることは容易に想像できます。Function::Parametersでは定義した関数から引数の数を含むメタ情報を抽出できるので,自動テストにこの検査を組込み,関数の引数の数が多すぎるコードを未然に防げます。

package Foo;
use Function::Parameters;

fun aaa($a, $b) { }
fun bbb($a, $b, $c, $d, $e) { }

package test;
use Test::More;
use Module::Functions;
use Function::Parameters;

subtest 'prohibit too many args' => sub {
    my $MAX = 4;
    my $pkg = 'Foo';
    my @functions = get_public_functions($pkg);
    for my $f (@functions) {
        my $name = "${pkg}::${f}";
        my $code = \&{$name};
        my $info = Function::Parameters::info $code;

        ok $info->args_max <= $MAX, $name;
        # =>
        # not ok 1 - Foo::bbb
        # ok 2 - Foo::aaa
    }
};
done_testing;

引数の個数の検査以外にも,型情報を用い,アプリケーションの事情を加味したコード検査もできます。堅牢生を高めるために,こういった実行時以外の検査を行えることは大きな利点です。

Function::Return─⁠─関数の戻り値の型制約

拙作のFunction::Return 0.02で,関数の処理結果も型制約でチェックできます。次のコードでは,関数の戻り値がStrかチェックしています。

use Function::Return;
use Types::Standard qw(Str);

sub hello :Return(Str) { $_[0] }

my $msg1 = hello('world');
my $msg2 = hello({}); # error!

Function::Parametersと組み合わせて使うこともでき,引数と戻り値が期待する型が一目でわかり,可読性の向上も望めます。

use Function::Parameters;
use Function::Return;
use Types::Standard -types;
use Test::More tests => 1;

fun Blood() {
    Str & sub { $_ =~ qr!(?:A|B|O)(?:A|B|O)! }
}

# 交配したときの血液型の組み合わせ
fun mate(Blood $a, Blood $b) :Return(ArrayRef[Blood]) {
    my @a = split //, $a;
    my @b = split //, $b;
    [ map {
        my $a = $_;
        map { join '', sort $a, $_ } @b } @a ];
}

is_deeply mate('AO', 'BO'), ['AB', 'AO', 'BO', 'OO'];
Function::Returnでメタ情報を取得する

Function::Parametersと同様に,Function::Returnも戻り値に関するメタ情報を取得でき,コード検査などに利用できます。現在のバージョンでは,$info->typesで指定した型制約を取り出せます。

use Function::Parameters;
use Function::Return;
use Types::Standard -types;

fun hello(Str $msg) :Return(Str) {
    return "HELLO $msg"
}

my $pinfo = Function::Parameters::info \&hello;
my $rinfo = Function::Return::info \&hello;

$rinfo->types; # [Str]
関数呼び出しのコンテキストの固定

Perlは関数の呼び出し方に応じて,関数の挙動を変更できます。具体的には,wantarrayの値に応じ,リストコンテキスト,スカラコンテキスト,ボイドコンテキストの分岐を行えます。おもしろいしくみで,慣れればコードを省力的に書くことを助けてくれます。

しかし,アプリケーションの開発においては,アプリケーションロジックやチームメンバーは流動的です。チームの皆が慣れた状況は作りにくいため,アプリケーション開発ではコンテキストに応じた挙動変更は避けたほうがよいと考えます。Function::Returnは,明示されている型制約を優先し,内部的に関数の呼び出しのコンテキストを固定し,この問題を回避しています。たとえば,次のように複数の型制約を指定した場合,リストコンテキストで呼び出すことを強要します。

use Function::Return;
use Types::Standard -types;
sub multi :Return(Num, Str) { 3.14, 'hello' }
my ($pi, $msg) = multi();
my $count = multi(); # ERROR! Required list context.

Perlに慣れている人からすると逆に不自然かもしれませんが,Num, Strで返却する約束を優先するようになっています。

Function::Returnのオプション設定

Function::ReturnReturnという関数の属性をエクスポートしますが,自分の好きに設定したい場合もあります。たとえば,次のコードはOutという名前に変更しています。

use Function::Return name => 'Out';
use Types::Standard -types;

sub foo :Out(Int) { 1.2 }
foo() # => ERROR! 1.2 is not INT.

以下はパフォーマンスを優先したい場合のオプションです。

use Function::Return no_check => 1;

sub foo :Return(Int) { 3.14 }
foo(); # NO ERROR!

戻り値の型チェックは,関数の処理が間違っていないかどうか確認するには便利ですが,本番環境ではパフォーマンスの都合で不要かもしれません。Function::Returnのオプションにno_checkを指定することで,型チェックをしないように変更できます。

まとめ

本稿では,始めに堅牢な開発を行うための予防策として,プラグマで構文チェックを厳しくする方法やPerl::Criticを用いた静的検査を紹介しました。次に,Type::Tinyで型制約を導入する方法を示しました。そして最後に,Function::ParametersFunction::Returnで関数の入出力を動的に型チェックし,さらに関数のメタ情報でコード検査する方法を紹介しました。

Perlの柔軟さを活かして,堅牢さを足すこともできると思っていただけたら幸いです。

さて,次回の執筆者は上川慶さんで,テーマは「Cを用いたPerl拡張入門」です。お楽しみに。

WEB+DB PRESS

本誌最新号をチェック!
WEB+DB PRESS Vol.108

2018年12月22日発売
B5判/168ページ
定価(本体1,480円+税)
ISBN978-4-297-10324-8

  • 特集1
    [効率急上昇!]スキーマ駆動Web API開発
    OpenAPI/GraphQLで仕様からコードもテストも作成
  • 特集2
    詳解PostgreSQL
    [10/11対応]現場で役立つ新機能と実践知識
  • 特集3
    ZOZO開発ノウハウ大公開
    既存資産を活かしたシステムリプレース
  • 18周年記念エッセイ
    壁の先に見えたもの
    限界! もうダメ! もはやこれまで!?

著者プロフィール

小林謙太(こばやしけんた)

東北大学大学院数学専攻修了後,2011年に株式会社モバイルファクトリーに新卒入社。

ソーシャルゲームの開発やプロダクトマネジメントに従事したのち,現在は,育成・採用・広報などのエンジニア組織の課題解決に取り組む。また,Japan Perl Associationのメンバーとして,YAPC::JapanやGotanda.pmなどのコミュニティを中心に活動している。

コメント

コメントの記入