sironekotoroの日記

Perl で楽をしたい

Perl で interface を理解するための長い旅

脚注からヒントを得る

ここしばらく「良いコード/悪いコードで学ぶ設計入門 ―保守しやすい 成長し続けるコードの書き方」をやっております。

gihyo.jp

その中でインターフェースって何?ということでググりつつ、適当にネットから拾いつつやっていたのですが、一つ日曜の午後に腰を据えてやってみることにしました。

普段の業務(による疲労)との兼ね合いもあり、なかなかこの本の学習が進まない・・・ので、ちょっと先を見たところ、6章巻末の脚注にこうありました。

interface は Java 以外では Kotlin や C# にもあります。Scala では trait という形で用意されています。

とあります。

trait !?

なんか、Perl での interface の実装を求めてググりまくってた時に色々引っかかってきた語です。

その時はスルーしていましたが・・・

トレイト (英: Trait) は、コンピュータープログラミングでの概念であり、専らオブジェクト指向プログラミングで用いられている。トレイトはメソッドの集合体であり、クラスの機能を拡張するために使われる[1][2]。 https://ja.wikipedia.org/wiki/%E3%83%88%E3%83%AC%E3%82%A4%E3%83%88

(中略)

Perl 5 では Moose モジュールで利用可能。なおロールの限定的な用途のみ「トレイト」と呼称し紛らわしい。

あ、繋がってきた。

gihyo.jp

この2009年の記事では Moose::Role での実装例が書かれていて参考になりました。

せっかくなので写経。ありがたいコードは写経しないとね。

Moose → Mouseにしたり、少し変えてるけどちゃんと動いた。

コメントは何ヶ月か先に見直した自分用に追加

ここクリックして展開

#!/usr/bin/env perl
use strict;
use warnings;
use Test::More tests => 4;

# Flyロールの定義
package Fly {
    use Mouse::Role;
    requires 'fly_with';    # Flyロールを継承するクラスが実装しているべきメソッド

    sub fly {
        my $self = shift;
        print "I can fly with " . $self->fly_with . ".\n";
    }
}

# 哺乳類クラス
package Mammal {
    use Mouse;
    sub produce_milk { print "i can produce milk.\n"; }

    __PACKAGE__->meta->make_immutable();
}

# こうもりクラス
package Bat {
    use Mouse;

    extends 'Mammal';    # 哺乳類クラスを継承
    with 'Fly';          # Flyロールを継承

    sub fly_with { 'wings'; }

    __PACKAGE__->meta->make_immutable();
}

package main;

my $bat = Bat->new();

# こうもりクラスには fly_with メソッドしかないのに、
# produce_milk, fly メソッドが利用できるようになっている。

can_ok( $bat => 'produce_milk' );    # ok 1 - Bat->can('produce_milk')
can_ok( $bat => 'fly' );             # ok 2 - Bat->can('fly')
ok( $bat->isa('Mammal') );           # ok 3
ok( !$bat->isa('Bird') );            # ok 4

さらに role を探究

これも2009年の記事です。当時は role が熱かった時期なのでしょうか。

その頃何してたかなぁ。

情報処理系の資格取りまくってた頃かな。

hiratara.hatenadiary.jp

こちらのコードも写経してみます。

このコードは複数の role を引き継いだ場合に、メソッドをどう解決しているのか?というものでした。

途中でエラーを出して、そこから逆算して仕組みを解説するあたりが好きな記事です。

ここクリックして展開

#!/usr/bin/env perl
use strict;
use warnings;

package Println {
    use Mouse::Role;

    requires 'write';

    sub println {
        my $self = shift;
        $self->write( @_, "\n" );
    }

    no Mouse::Role;
}

package Logging {
    use Mouse::Role;

    with 'Println';
    requires 'println';

    sub log {
        my $self = shift;
        $self->println( scalar localtime() . ' ' . $_ ) for @_;
    }

    no Mouse::Role;
}

package HelloWorld {
    use Mouse::Role;

    requires 'println';

    sub helloWorld {
        my $self = shift;
        $self->println("Hello World!");
    }
    no Mouse::Role;
}

package MyApp {
    use Mouse;

    with 'HelloWorld', 'Logging';

    # この write メソッドをコメントアウトして無効にすると、
    # write がない旨のエラーが Println ロールから出る。
    # しかし、HelloWorld ロールが要求する println メソッドは、同時に
    # 継承した Logging ロールから継承して解決されているのでエラーにならない。
    sub write {
        my $self = shift;
        warn @_;
    }

    __PACKAGE__->meta->make_immutable();
}

package main;
my $myapp = MyApp->new();

# MyApp クラスには helloworld メソッドも log メソッドもないが、
# role から継承しているので実行できる!
$myapp->helloWorld();
$myapp->log("said 'hello'");

Moose::Roleの「未実装のメソッドを教えてくれる」と言うJavaのInterfaceっぽい機能*6は、Traitsを実現するためのおまけでした。

なるほど、まさに「未実装のメソッドを教えてくれる」というところにしか注目していなかったですわ。

role の組み合わせで、よりスリムなクラス設計ができそう(能力があれば)。

Function::Interface

名前そのものに Interface が入ったモジュール。早速 cpanm でインストール!

$ cpanm Function::Interface

あれ?エラーだ

テストで t/03 の Function::Return::info 使ってるところが軒並み落ちてる。

でも、plenv で Perl 5.32.0 だと普通にインストールできる。

Perl 5.34, Perl 5.36 だとこのテストで引っかかった。

ここクリックして展開

cp lib/Function/Interface.pm blib/lib/Function/Interface.pm
cp lib/Function/Interface/Info/Function/Param.pm blib/lib/Function/Interface/Info/Function/Param.pm
cp lib/Function/Interface/Info.pm blib/lib/Function/Interface/Info.pm
cp lib/Function/Interface/Impl.pm blib/lib/Function/Interface/Impl.pm
cp lib/Function/Interface/Info/Function.pm blib/lib/Function/Interface/Info/Function.pm
cp lib/Function/Interface/Info/Function/ReturnParam.pm blib/lib/Function/Interface/Info/Function/ReturnParam.pm
cp lib/Function/Interface/Types.pm blib/lib/Function/Interface/Types.pm
t/01_function_interface/assert_valid_interface_params.t .. ok
t/01_function_interface/assert_valid_interface_return.t .. ok
t/01_function_interface/import.t ......................... ok
t/01_function_interface/import_options.t ................. ok
t/01_function_interface/info.t ........................... ok
t/01_function_interface/unimport.t ....................... ok
t/02_function_interface_info/function.t .................. ok
t/02_function_interface_info/function/param.t ............ ok
t/02_function_interface_info/function/return_param.t ..... ok
t/02_function_interface_info/info.t ...................... ok
t/03_function_interface_impl/assert_valid.t .............. Undefined subroutine &Function::Return::info called at /Users/sironekotoro/p5-Function-Interface/.build/046cqH9S/blib/lib/Function/Interface/Impl.pm line 110.
Compilation failed in require at t/03_function_interface_impl/assert_valid.t line 16.
BEGIN failed--compilation aborted at t/03_function_interface_impl/assert_valid.t line 16.
t/03_function_interface_impl/assert_valid.t .............. Dubious, test returned 255 (wstat 65280, 0xff00)
No subtests run
t/03_function_interface_impl/case_duplicate.t ............ ok
t/03_function_interface_impl/check_params.t .............. ok
t/03_function_interface_impl/check_return.t .............. 1/?
# Failed test 'empty return'
# at t/03_function_interface_impl/check_return.t line 14.
# Caught exception in subtest: Undefined subroutine &Function::Return::info called at t/03_function_interface_impl/check_return.t line 37.

# Failed test 'single return'
# at t/03_function_interface_impl/check_return.t line 21.
# Caught exception in subtest: Undefined subroutine &Function::Return::info called at t/03_function_interface_impl/check_return.t line 37.

# Failed test 'two return'
# at t/03_function_interface_impl/check_return.t line 32.
# Caught exception in subtest: Undefined subroutine &Function::Return::info called at t/03_function_interface_impl/check_return.t line 37.
# Seeded srand with seed '20220612' from local date.
t/03_function_interface_impl/check_return.t .............. Dubious, test returned 3 (wstat 768, 0x300)
Failed 3/3 subtests
t/03_function_interface_impl/error.t ..................... ok
t/03_function_interface_impl/impl_of.t ................... 1/? implements error: cannot get function `foo` parameters info. Interface: fun foo() :Return() at t/03_function_interface_impl/impl_of.t line 21
    died at /Users/sironekotoro/p5-Function-Interface/.build/046cqH9S/blib/lib/Function/Interface/Impl.pm line 95.
Execution of t/03_function_interface_impl/impl_of.t aborted due to compilation errors.

# Failed test at t/03_function_interface_impl/impl_of.t line 7.

# Failed test at t/03_function_interface_impl/impl_of.t line 8.
# Tests were run but no plan was declared and done_testing() was not seen.
# Looks like your test exited with 255 after test #6.
# Seeded srand with seed '20220612' from local date.
t/03_function_interface_impl/impl_of.t ................... Dubious, test returned 255 (wstat 65280, 0xff00)
Failed 2/6 subtests
t/03_function_interface_impl/import.t .................... ok
t/03_function_interface_impl/info_interface.t ............ ok
t/03_function_interface_impl/info_params.t ............... ok
t/03_function_interface_impl/info_return.t ............... Undefined subroutine &Function::Return::info called at /Users/sironekotoro/p5-Function-Interface/.build/046cqH9S/blib/lib/Function/Interface/Impl.pm line 110.
t/03_function_interface_impl/info_return.t ............... Dubious, test returned 255 (wstat 65280, 0xff00)
No subtests run
t/03_function_interface_impl/register_check_list.t ....... ok
t/04_function_interface_types/impl_of.t .................. implements error: cannot get function `foo` parameters info. Interface: fun foo() :Return() at t/lib/Foo.pm line 2
    died at /Users/sironekotoro/p5-Function-Interface/.build/046cqH9S/blib/lib/Function/Interface/Impl.pm line 95.
Compilation failed in require at t/04_function_interface_types/impl_of.t line 7.
BEGIN failed--compilation aborted at t/04_function_interface_types/impl_of.t line 7.
t/04_function_interface_types/impl_of.t .................. Dubious, test returned 255 (wstat 65280, 0xff00)
No subtests run

Test Summary Report
-------------------
t/03_function_interface_impl/assert_valid.t            (Wstat: 65280 Tests: 0 Failed: 0)
  Non-zero exit status: 255
  Parse errors: No plan found in TAP output
t/03_function_interface_impl/check_return.t            (Wstat: 768 Tests: 3 Failed: 3)
  Failed tests:  1-3
  Non-zero exit status: 3
t/03_function_interface_impl/impl_of.t                 (Wstat: 65280 Tests: 6 Failed: 2)
  Failed tests:  5-6
  Non-zero exit status: 255
  Parse errors: No plan found in TAP output
t/03_function_interface_impl/info_return.t             (Wstat: 65280 Tests: 0 Failed: 0)
  Non-zero exit status: 255
  Parse errors: No plan found in TAP output
t/04_function_interface_types/impl_of.t                (Wstat: 65280 Tests: 0 Failed: 0)
  Non-zero exit status: 255
  Parse errors: No plan found in TAP output

まぁ、Perl 5.32 で動いてるなら、Perl 5.36 でもいけるでしょう・・・ということで、ノーテストでインストール。

$ cpanm -n Function::Interface

無事、Perl 5.36 の環境にインストールできたので、早速例示コードを試す!

metacpan.org

Perl 5.36 で例示のコードを動かそうとしたものの、エラー。

implements error: cannot get function `hello` parameters info. Interface: fun hello(Str $msg) :Return(Str) at interface.pl line 35
    died at /Users/sironekotoro/.anyenv/envs/plenv/versions/5.36.0/lib/perl5/site_perl/5.36.0/Function/Interface/Impl.pm line 95.
Execution of interface.pl aborted due to compilation errors.

なお、モジュールインストール時にエラーの出なかった Perl 5.32 でもエラー。

あれー・・・

というわけで、Perl 5.32 の環境でエラーを追っかけて、例示のコードに手を加えて動くところまでやってみました。

fun じゃなくて method にしたのと、例示のコードに加えて add も実装してみました。

FooService のところがまだうまく理解できない感です。

ここクリックして展開

#!/usr/bin/env perl
use strict;
use warnings;
use lib qw/./;

# インターフェース定義
package IFoo {
    use Function::Interface;
    use Types::Standard -types;
 
    method hello(Str $msg) :Return(Str);
 
    method add(Int $a, Int $b) :Return(Int);
}

# クラスにインターフェースを実装する
package Foo {
    use Function::Interface::Impl qw(IFoo);
    use Types::Standard -types;
    use Mouse;
 
    method hello(Str $msg) :Return(Str) {
        return "HELLO $msg";
    }
 
    method add(Int $a, Int $b) :Return(Int) {
        return $a + $b;
    }
}

# インターフェースを適用させたメソッドを使う
package FooService {
    use Function::Interface::Types qw(ImplOf);
    use Function::Parameters;
    use Function::Return;
    use Mouse;
 
    use aliased 'IFoo';
 
    method greet(ImplOf[IFoo] $foo) :Return() {
        print $foo->hello('World!') . "\n";
        return;
    }

    method add(ImplOf[IFoo] $foo ) :Return(){
        print $foo->add(40, 2) . "\n";
        return;        
    }


}

my $foo_service = FooService->new;
my $foo = Foo->new;
 
$foo_service->greet($foo);  # HELLO World!


$foo_service->add($foo);  # 42

1

しかし、このコードは Perl 5.32 では動くけど、 Perl 5.36 では動かないのだった・・・ううーん

implements error: cannot get function `hello` parameters info. Interface: method hello(Str $msg) :Return(Str) at /Users/sironekotoro/Dropbox/study/良いコード/悪いコードで学ぶ設計入門/role/FooAll.pm line 36
    died at /Users/sironekotoro/.anyenv/envs/plenv/versions/5.36.0/lib/perl5/site_perl/5.36.0/Function/Interface/Impl.pm line 95.
BEGIN not safe after errors--compilation aborted at /Users/sironekotoro/Dropbox/study/良いコード/悪いコードで学ぶ設計入門/role/FooAll.pm line 51.