Perl5

Perlで静的型付プログラミング

More than 1 year has passed since last update.

この記事はモバイルファクトリー Advent Calendar 2017の3日目の記事です。
1日目、2日目に引き続いて、@carimaticsが担当します。

2日目の記事は『Perlでオブジェクト指向プログラミング』でした。

本記事では、 Kavorka というモジュールを使ってPerlで静的型付プログラミング機構を導入します。
「動的型付言語と静的型付言語、どっちが優れているか」みたいな話はしません。

2日目の記事の内容をベースにしますので、未読の方は是非お読みください。

開発環境

以下の環境で動作確認をしています。

  • OS: macOS Sierra version 10.12.6
  • Perl: perl v5.26.0
  • Carton: carton v1.0.28
  • Docker: Version 17.09.0-ce-mac35
    • Docker image: perl:5.26

本記事のコードは以下のコマンドで実行されるDockerコンテナで動かしました。

$ docker run -it perl:5.26 bash

準備

2日目の記事の準備と同様に、Cartonのインストール、作業ディレクトリの作成をしてください。

依存モジュールのインストール

作業ディレクトリ上で、以下のように cpanfile を記述します。

Work/cpanfile
requires 'Data::Validator';
requires 'Kavorka';
requires 'Mouse';
requires 'MouseX::Types::Mouse';

依存モジュールをインストールします。

$ carton install

(中略)

Complete! Modules were installed into Work

Perlで静的型付プログラミング

前述の通り、「動的型付言語と静的型付言語、どっちが優れているか」という話はしません。
私はとにかく型が欲しいです。

Kavorka

先にお断りしておくと、このモジュールは開発段階で、多くのバグが潜んでいる可能性があるとのことです。

Kavorka is still at a very early stage of development; there are likely to be many bugs that still need to be shaken out. Certain syntax features are a little odd and may need to be changed in incompatible ways.

Kavorka - function signatures with the lure of the animal - metacpan.org

ではどうしてこのモジュールを紹介するかというと、これを使うと引数の型チェックを行うコードがかなりスッキリするからです。
引数の明示という観点からのPerlモジュール群という記事で色々なモジュールが紹介されていますが、やっぱりノイズが多くて冗長な感じがします。
sub method(Type $param) {...} こんな感じで書きたいですよね?
それを実現するのが Kavorka です。

では、2日目の記事Ruler クラスを見てみます。

Work/lib/Ruler.pm
package Ruler {
    use Mouse;

    has p1 => (is => 'rw', isa => 'Point');
    has p2 => (is => 'rw', isa => 'Point');

    sub distance {
        my $self = shift;

        my $dx = $self->p2->x - $self->p1->x;
        my $dy = $self->p2->y - $self->p1->y;

        return sqrt(($dx**2) + ($dy**2));
    }
};

1;

相変わらず酷いコードです。
まずリファクタリングしましょう。

Work/lib/Ruler.pm
package Ruler {
    use Mouse;

    sub distance {
        my $self = shift;
        my ($p1, $p2) = @_;

        my $dx = $p2->x - $p1->x;
        my $dy = $p2->y - $p1->y;

        return sqrt(($dx**2) + ($dy**2));
    }
};

1;

わざわざ xy を属性に持たせる必要はありません。
p1 、 p2 は引数で受け取ればいいですし、そのほうが自然です。
しかし困ったことに、このコードでは受け取ったものが Point かどうかチェックしていません。

Data::Validator を利用してチェックすると、以下のように書けます。

Work/lib/Point.pm
package Ruler {
    use Mouse;

    use Data::Validator;

    sub distance {
        my $v = Data::Validator->new(
            p1 => 'Point',
            p2 => 'Point',
        )->with(qw/Method Sequenced/);
        my ($self, $args) = $v->validate(@_);
        my ($p1, $p2) = @$args{qw/p1 p2/};

        my $dx = $p2->x - $p1->x;
        my $dy = $p2->y - $p1->y;

        return sqrt(($dx**2) + ($dy**2));
    }
};

1;

うーん、これは…。
型のチェックだけでコード量が膨れ上がっています。
処理より型チェックのコードのほうが記述量が多いです。
snippetの助けがあってもこんなコードを何度も書くのは心が折れます。

まぁ何はともあれ使ってみましょう。

Work/script/ruler.pl
use Ruler;
use Point;
use ColorPoint;

my $p1 = Point->new(x => 0, y => 0);
my $p2 = ColorPoint->new(x => 3, y => 4, color => 'blue');

my $ruler = Ruler->new();
say "distance: ", $ruler->distance($p1, $p2);
$ carton exec -- perl script/ruler.pl
distance: 5

さて、前置きが長くなりましたが本題です。
Kavorka で書き直してみましょう。

Work/lib/KavorkaRuler.pm
package KavorkaRuler {
    use Mouse;
    use Kavorka;

    use Point;

    method distance(Point $p1, Point $p2) {
        my $dx = $p2->x - $p1->x;
        my $dy = $p2->y - $p1->y;

        return sqrt(($dx**2) + ($dy**2));
    }
};

1;

めちゃくちゃスマートになりましたね!
diffを見るとこんな感じです。

-package Ruler {
+package KavorkaRuler {
     use Mouse;
+    use Kavorka;

+    use Point;
-    use Data::Validator;
-
-    sub distance {
-        my $v = Data::Validator->new(
-            p1 => 'Point',
-            p2 => 'Point',
-        )->with(qw/Method Sequenced/);
-        my ($self, $args) = $v->validate(@_);
-        my ($p1, $p2) = @$args{qw/p1 p2/};
+    method distance(Point $p1, Point $p2) {
         my $dx = $p2->x - $p1->x;
         my $dy = $p2->y - $p1->y;

もちろん実行もできます。

Work/script/kavorka_ruler.pl
use KavorkaRuler;
use Point;
use ColorPoint;

my $p1 = Point->new(x => 0, y => 0);
my $p2 = ColorPoint->new(x => 3, y => 4, color => 'blue');

my $ruler = KavorkaRuler->new();
say "distance: ", $ruler->distance($p1, $p2);
$ carton exec -- perl script/kavorka_ruler.pl
distance: 5

あとは返り値の型情報も欲しくないですか?
欲しいですよね!
書きましょう!

Work/script/kavorka_ruler.pl
package KavorkaRuler {
    use Mouse;
    use Kavorka;

    use Point;

    method distance(Point $p1, Point $p2 --> Int) {
        my $dx = $p2->x - $p1->x;
        my $dy = $p2->y - $p1->y;

        return sqrt(($dx**2) + ($dy**2));
    }
};

1;

引数のかっこの最後に --> Int なるものが追加されました。
これで戻り値の肩を明示できます。

本当に型チェックが行われいてるかどうか、試してみましょう。

Work/script/kavorka_ruler.pl
package KavorkaRuler {
    use Mouse;
    use Kavorka;

    use Point;

    method distance(Point $p1, Point $p2 --> Int) {
        my $dx = $p2->x - $p1->x;
        my $dy = $p2->y - $p1->y;

        #return sqrt(($dx**2) + ($dy**2));
        return 'foobar';
    }
};

1;
$ carton exec -- perl script/kavorka_ruler.pl
Reference ["foobar"] did not pass type constraint "ArrayRef[Int]" at script/kavorka_ruler.pl line 15

ArrayRef[Int] じゃないよ!って怒られます。
なんで ArrayRef になってるのかは謎です。
試しに ArrayRef[Int] 返してみます。

Work/lib/KavorkaRuler.pm
package KavorkaRuler {
    use Mouse;
    use Kavorka;

    use Point;

    method distance(Point $p1, Point $p2 --> Int) {
        my $dx = $p2->x - $p1->x;
        my $dy = $p2->y - $p1->y;

        # return ArrayRef
        return [ sqrt(($dx**2) + ($dy**2)) ];
    }
};

1;
$ carton exec -- perl script/kavorka_ruler.pl
Reference [[5]] did not pass type constraint "ArrayRef[Int]" at script/kavorka_ruler.pl line 15

型チェックをするときに ArrayRef で包んでどこかでチェックしているっぽいですね…?

まとめ

Kavorkaというモジュールを使うといい感じに型チェックができるよ、というお話をしました。
しかし、まだ開発段階らしいので、利用には十分注意してください。

こんな非Perlっぽい記述をしてまでPerlにこだわるとは、Perl Mongerをここまで駆り立てるほどの何かがあるのでしょう。
個人的にはわざわざここまで頑張るくらいならまともな静的型付言語を使ったほうがいいかなぁ…、と思いました。

モバイルファクトリー Advent Calender 2017、初日から3日連続で@carimaticsが担当しました。
お付き合いいただきありがとうございました。

明日は @akihiro_0228 さんです。
よろしくお願いします。