Perl Hackers Hub

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

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

関数に型制約を導入する

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

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

Type::TinyにはType::Paramsという引数チェック用ライブラリが同梱されていますが、本稿ではFunction::Parameters 2.001003を解説します。Function::Parametersはパフォーマンスこそ若干Type::Paramsに劣るものの[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.130

2022年8月24日発売
B5判/168ページ
定価1,628円
(本体1,480円+税10%)
ISBN978-4-297-13000-8

  • 特集1
    イミュータブルデータモデルで始める
    実践データモデリング

    業務の複雑さをシンプルに表現!
  • 特集2
    いまはじめるFlutter
    iOS/Android両対応アプリを開発してみよう
  • 特集3
    作って学ぶWeb3
    ブロックチェーン、スマートコントラクト、NFT

おすすめ記事

記事・ニュース一覧