Perl Hackers Hub

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

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

動的な型制約の応用例

前節までで、Perlで動的な型制約を導入する方法の基本は紹介し終えました。本節では、動的な型制約をアプリケーションで応用した例を紹介します。

アプリケーションの仕様を型として定義する

アプリケーションには、満たすべき仕様があります。たとえばユーザー名は3文字以上100文字以内である必要がある、ブログの状態は公開か非公開のどちらかであるなどです。Webアプリケーションとしてユーザーから入力を受け付けるときも、開発者がコマンドラインを使いデータを入れるときも、どんなときでも仕様を満たさない不整合なデータを作りたくありません。そのためには、たとえばデータベースレベルで制限をかけるなど、さまざまな方法が考えられます。ただ、簡単な仕様なら、これまで紹介した動的な型制約を利用して実現できます。

リスト3は、ブログアプリケーションの仕様を型として定義し、制約をかけることでチェックしている例です。(1)でブログのタイトルは3文字以上100文字以内という仕様を型として定義しています。(2)はブログの状態は公開を表すpublicか非公開を表すprivateのどちらかであると定義しています。(3)はブログデータを更新するメソッドで、定義したBlog::TitleやBlog::Statusを利用して引数に制約をかけています。これで、update_blogに変なデータを渡したとしても、仕様を満たさないデータが作られることはありません。

リスト3 仕様を型として定義する
package Blog;

use Smart::Args;
use Mouse::Util::TypeConstraints;

subtype 'Blog::Title',                           
    => as 'Str'                                  
    => where {                                   
        3 <= length($_) && length($_) <= 100;    
    }                                            ┣(1)
    => message {                                 
        "$_ is not valid blog title";            
    };                                           

enum 'Blog::Status' => [qw(public private)];  ━(2)

sub update_blog {                                
    args my $class => 'ClassName',               
         my $blog_id => 'Int',                   
         my $title => 'Blog::Title',             ┣(3)
         my $status => 'Blog::Status';           
    # 渡されたデータでのブログデータの更新処理   
    ...                                          
}                                                

この例のように、定義した型とSmart::Argsを使ってロジックの関数の引数に型制約をかけておけば、単純な仕様であれば引数を間違えて不整合なデータを作ってしまうことを防げます。もちろんユーザーの入力を受け付ける場合、そのまま関数に渡すとユーザーにとって意味のないSmart::Argsのエラーが表示されるため、ユーザーを混乱させてしまいます。そのため使いやすさの観点からは、関数に渡す前にユーザーの入力が正しいかどうかをチェックし、誤っていた場合は適切なフィードバックを返すということを別に行う必要があります。

型制約でユーザーの入力を制限する

先ほど、使いやすさの観点からは、関数に渡す前にユーザーの入力が正しいかどうかをチェックし、誤っていた場合は適切なフィードバックを返すということを別に行う必要があると説明しました。もし返すべきフィードバックが単純なものであれば、型制約を用いてチェックする方法もあります。ユーザーから受け取った入力が型制約を満たすかをチェックし、満たさない場合にフィードバックを返すという方法です。

型制約を満たすかどうかを真偽値で取得できれば、このようなチェックを行えるはずです。この方法を実現するには、Mouse::Util::TypeConstraintsを利用します。Mouse::Util::TypeConstraintsにはfind_type_constraintという型を取得するユーティリティがあり、これを用いると型のオブジェクトを取得できます。さらにそのオブジェクトからcheckメソッドを呼ぶと、型制約を満たすかを真偽値で返してくれます。

次のコードは、前項で定義したBlog::Title型とfind_type_constraintを利用して、ユーザーの入力をチェックする例です。

use Mouse::Util::TypeConstraints;
my $input = ...;

my $is_valid = find_type_constraint('Blog::Title')-
>check($input);  ━(1)
if (!$is_valid) {
    # ユーザーにエラーメッセージを返す
}

(1)のように呼び出せば、$inputBlog::Title型を満たすなら真が、満たさないなら偽が$is_validに代入されます。偽の場合にユーザーにフィードバックを返せば、最低限のユーザー入力のチェックを行えます。

型制約で不正なリクエストパラメータを制限する

Webアプリケーションを作っていると、悪意のあるユーザーが不正なリクエストパラメータを送ってくる場合があります。不正なものをそのまま受け入れるとアプリケーションに不具合を起こすこともあるため、チェックが必要です。

たとえばブログの状態を公開publicにするか、非公開privateにするかをユーザーがWeb上で選べ、それがHTMLのセレクトボックスで実装されているとします。

<select name="blog_status">
  <option value="public">公開</option>
  <option value="private">非公開</option>
</select>

このとき、普通に利用しているユーザーからは、blog_statusとしてpublicprivateのどちらかしか送られません。しかし悪意のあるユーザーの場合、別の文字列を送ってくる可能性があります。

このような悪意のあるパラメータをいちいちチェックするのは大変です。チェックをする方法はいろいろありますが、型制約を応用して簡単にチェックすることもできます。

リスト4は、HTTPリクエストを処理するモジュールのPlack::Requestを継承し、typed_paramというメソッドを定義したものです。typed_paramメソッドは、パラメータ名と型名を渡せば、型制約を満たすならその値を、そうでなければundefを返します。

リスト4 typed_paramの実装
package My::Request;
use parent qw(Plack::Request);
use Mouse::Util::TypeConstraints;

sub typed_param {
    my ($self, $key, $type) = @_;

    # リクエストパラメータがなければundefを返す
    my $val = $self->parameters->get($key) // return undef;

    # パラメータが制約を満たすならそのパラメータを、
    # 満たさないならundefを返す
    return find_type_constraint($type)->check($val) ?
        $val : undef;
}

このtyped_paramとリスト3で定義したBlog::Status型を利用すれば、普通のユーザーの利用なら正しくpublicかprivateの値を受け取り、悪意のあるユーザーがevilのような値を送ってきたらundefを受け取れます。undefが返ってきている場合は不正なリクエストであると判断できるため、エラー処理を単純化できます。typed_paramメソッドの利用例は次のとおりです。

my $req = My::Request->new($env);

my $status = $req->typed_param(     
    'blog_status', 'Blog::Status',  ┣(1)
);                                  
if (! defined $status) {
    # Bad Requestなどを返す
}

(1)のように、受け取りたいリクエストパラメータ名blog_statusを第1引数に、Blog::Statusを第2引数に渡します。Blog::Status型がpublicprivateしか受け付けないため、悪意のあるユーザーがevilという値を送信してきたら、変数$statusにはundefが代入されます。undefの場合にBad Requestを返すなどの処理をすれば、不正なパラメータを防げます。

動的な型制約を導入してみて

筆者は、所属するはてなの最近のプロジェクトで今回紹介した動的な型制約を導入し、半年間ほど運用しました。運用してみると、だんだん導入のメリットやデメリットが見えてきました。そこで本節では、筆者が感じたメリットやデメリットを紹介します。

メリット

動的な型制約の導入によって感じたメリットは以下の3つです。

関数の使い方の間違いで不整合が起こらない

最初に紹介したとおり、関数を間違って使っても、それだけではデータの不整合が起こらないことは大きなメリットです。

長くアプリケーションを運用していると、関数を定義する人と関数を呼び出す人は別であることも多くなります。自分が定義した関数でも、長く触っていない部分であれば、どんな関数であったか忘れてしまいます。結果として、呼び出し方を間違えることは頻繁に起こります。

引数に型制約をかければ、呼び出しの間違いで関数が中途半端に実行されることはなく、呼び出し間違いによるデータの不整合は起こらなくなりました。

関数に何を渡せばよいかが明確になる

関数の引数に何を渡せばよいかわかりやすくなるメリットも感じました。

動的な型制約を導入していない状態では、引数にどんなオブジェクトを渡すのか、どのパラメータが必須なのかをPerlのシンタックスから判断できません。そのため引数の変数名か、引数に何を渡すか書かれたコメントから判断するしかありませんでした。

Smart::Argsで型制約をかけておけば、Smart::Argsのシンタックスによって渡すべき型が明示され、何を渡せばよいかすぐに判断できるようになりました。

誰が書いても厳しくチェックされる

Smart::Argsを導入する前も、必須の引数がない場合は例外を出し、関数が中途半端に実行されないようにする対策は行っていました。しかし、このやり方は人によってどこまでチェックするかがまちまちで、場合によってはまったくチェックしていないこともありました。

Smart::Argsは、簡単な記法で引数の型が正しいことのチェックを行えます。そのため、プロジェクトメンバーの誰もが型制約を書くようになり、関数の呼び出し間違いのチェックが必ず行われるようになりました。

デメリット

型制約を導入するデメリットはほとんどありませんでした。1つだけ挙げるとすると、呼び出しの回数が多い場合に遅くなることです。

呼び出しの回数が多いと遅くなる

静的言語での型チェックはコンパイル時に行われ、型チェックで実行が遅くなることはありません。しかし動的な型チェックはプログラムの実行時に行われるため、関数を実行するたびにチェックも実行されます。

Smart::ArgsやMouseの型制約のしくみは、実行時に動作することに配慮して高速に動作します。それでも数万回も呼び出すと、体感できるほどに実行が遅くなります。

ベンチマークのコードはリスト5で、実行結果は以下です。

             Rate     args no_args
args      80645/s       -- -98%
no_args 3333333/s    4033% --
リスト5 Smart::Argsのベンチマーク
use Smart::Args;
use Benchmark qw(cmpthese);

sub no_args_func {
    my ($num1, $num2, $num3, $num4) = @_;
    return $num1 + $num2 + $num3 + $num4;
}

sub args_func {
    args_pos my $num1 => 'Num',
             my $num2 => 'Num',
             my $num3 => 'Num',
             my $num4 => 'Num';

    return $num1 + $num2 + $num3 + $num4;
}

cmpthese(100000, {
    no_args => sub {
        no_args_func(100, 200, 300, 400);
    },
    args => sub {
        args_func(100, 200, 300, 400);
    },
});

チェックをしていないno_args_funcは変数への代入と加算しかしていないので当然速く、秒間333万回も実行できます。しかしargs_funcのようにNum型だけを使ったチェックを入れるだけで、だいたい秒間8万回ほどしか実行できなくなります。

秒間数万回実行できるのは十分速いですが、非常に多く呼ばれる関数への型制約の導入は慎重に検討したほうがよいでしょう。

まとめ

本稿では、Perlで動的な型制約を導入する動機や、Smart::Argsを使って型制約を導入する方法、型制約の応用など、Perlの動的な型制約について紹介しました。Smart::Argsの導入は簡単ですし、少しの手間でアプリケーションの堅牢性を高めることができるので、ぜひ使ってみてください。

さて、次回の執筆者は岡林大さんで、テーマはPlack::Middlewareです。お楽しみに。

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

おすすめ記事

記事・ニュース一覧