Blog::kobaken

prove t/foo/bar/baz.t

Perlのレキシカル関数の使い所2選

この記事はPerl アドベントカレンダー 2023のN日目の記事です。娘はサンタさんにキックボードをねだっていました。


レキシカル関数というと、ブロックやファイルだったり特定のスコープでだけ有効な関数のことで、スコープ外では存在しないように扱えます。 Perlの場合、my sub SUBNAME { ... }のように書くとレキシカル関数が定義できて、v5.26から正式機能になっています。

{
   # このブロックの中だけ、helloは有効
   my sub hello { "Hi!" }
   hello(); # Hi!
}
hello(); # ERROR: Undefined subroutine &main::hello

...が、無名関数を使えば用を足せると思って、機能が正式化してから6年経ちますが全然使ってなかったです。今は、めっちゃ便利じゃんと思い直したので、おすすめ?の使い方を書きます。

プライベートメソッドとして

プライベートメソッドとしての利用は、レキシカル関数のTHE・想定通りの使い方だと思います。Perlは慣習的に、メソッドを外部パッケージから呼び出さないように隠蔽したい時、「_private」のように関数名にアンダースコアをつけて「これはプライベートメソッドだから外から絶対呼び出さないでね!」と目印をつけ紳士協定で生活します。けど、ただの目印に過ぎないので、アンダースコアがついていようと呼び出せるわけで、当然?紳士協定をやぶる輩(自分含め)がでてきます。そんな紳士協定違反ができないコーディングパターンはあるにはありますが、自分はしっくりこなかったです。(my $private = sub { ...} のように無名関数を利用したり、Class::InsideOutのようなinside-outパターンのOOPモジュールを利用など。紳士協定違反の予防と、ビルトインと異なるオブジェクトの記法を採用の2つを見比べると、バランスが悪いなーと)

けど、今はプライベートメソッドが欲しければ、既存の関数定義にちょっと足すだけで実現できます。

package Foo {
   #  レキシカル関数を使って、外部から呼べないメソッドを定義
   my sub private_method($class) {
       ... 
   }

   sub public_method($class) {
       $class->private()
       # do something
   }
}

Foo->public_method;
Foo->private_method; # ERROR: Undefined subroutine &Foo::private_method

ユニットテストを読みやすくするため

通常の関数定義だと、パッケージ内に同じ名前の関数を何個も作ることはできません。が、レキシカル関数であれば、パッケージ内に同じ名前の関数をいくつも定義できます。この性質が、ユニットテストと相性良きです。具体的にはこんな感じ。

use v5.38;
use Test2::V0;

subtest 'add' => sub {
    # add関数をテストする、テスト主題を用意
    my sub subject($a, $b) { add($a, $b) }

    is subject(1, 2), 3;
    is subject(4, 5), 6;
};

subtest 'baz' => sub {
    # 2個目のsubject 関数の定義だけど、1個目の定義とスコープが違うので定義できる
    my sub subject($a, $b) { ... }

    # 期待値と一致するか確認するのに記述が複雑な場合は、テストユーティリティを用意するのも手
    my sub run_test($a, $b, $expected, $testname) {
        my $ctx = context();

        my $result = subject($a, $b);
        is $result, object {
            prop blessed => 'Some::Result';
            call value => $expected;
        }, $testname;
        $ctx->release;
    }

    run_test(1, 2 => 3, 'Case1');
    run_test(4, 5 => 6, 'Case2');
};

done_testing;

このコードでは、テストの主題やテストユーティリティのレキシカル関数を定義していますが、テストの入力と期待値が明確になっているので良いなーと。subjectといったお決まりの用語を決めれば、テストの読み下しもしやすくなると思います。rspecっぽさがありますね。また、個人的に良いと思うのが、このコードは、言語のビルドインの機能だけで実現できてる点です。perlrspecっぽく記述できるTest2::Tools::Specというフレームワークがありますが、こういったフレームワークの中身を熟読し、DSLの限界を探るなんてことは不要です。ただ、そこにレキシカル関数があるだけです。

さいごに

Perlのレキシカル関数の使い所を2つ紹介しました。3つ揃うと気持ちが良いので、他の使い所があれば、良かったらコメントで教えてください!