MPがありません。

$liiu->mp == 0

Perl5.32の新機能

 6/22にリリースされたPerlの最新安定バージョン5.32では久しぶりにいろいろ新機能が追加されてたので、perldeltaを読みつつ試してみました。
 内容は7/1にあったPerl若手の会で発表したこととほぼ同じで、そこで聞いたことを追記したりしてます。

isa 演算子

 左被演算子に渡した値が右被演算子のクラスのインスタンスまたはそこから派生したクラスのインスタンスなのかを調べる演算子です。
 use feature 'isa' でこの機能を有効化でき、実験的機能使用による警告は no warnings 'experimental::isa' で抑止できます。

use v5.32;
use feature 'isa';
no warnings 'experimental::isa';
  
package A { use Moo }
my $obj = A->new;
  
say '$obj is instance of A.' if $obj isa A;
say '$obj is instance of A.' if $obj isa 'A';
my $klass = 'A';
say '$obj is instance of A.' if $obj isa $klass;

 isa演算子の追加によって、トリッキーな方法や外部モジュールを利用することなく、クラスのインスタンスまたはそこから派生したクラスのインスタンスなのかを調べることが可能になります。

 Perlには以前から UNIVERSAL に isa メソッドが定義されていて、すべてのクラスから呼べるこのメソッドを使うことで呼び出し元が引数に与えたクラスのインスタンスまたはそこから派生したクラスのインスタンスなのかを調べていました。

package A { use Moo }
   
my $obj = A->new;
say '$obj is instance of A.' if $obj->isa('A');

 しかし isa メソッドを呼ぶ処理を書くだけではクラスのインスタンスまたはそこから派生したクラスのインスタンスなのかを調べることができないので不便なことがあります。

 まず、未定義値や空文字列からメソッドを呼び出そうとすると例外が発生するので、それらの値が来るような処理では例外が発生しないように処理を書く必要がありました。
 そのような処理を書くために、 UNIVERSAL::isa を関数として呼び出したり*1Scalar::Util モジュールにある blessed 関数を利用して bless されたリファレンスかをチェックしてから isa メソッドを呼び出すといったことが行われてきました。

my $obj = undef;
$obj->isa('A'); # Can't call method "isa" on an undefined value 

$obj = '';
$obj->isa('A'); # Can't call method "isa" without a package or object reference 

# UNIVERSAL::isa を関数として呼び出して判定
UNIVERSAL::isa($obj);

# blessed でblessされたインスタンスか調べてから判定
use Scalar::Util qw( blessed );
say '$obj is instance of A.' if blessed($obj) && $obj->isa('A');

 また、 isa メソッドは文字列からも呼び出せる為クラスのインスタンスまたはそこから派生したクラスのインスタンスかどうかを調べたい場合は不都合があり、そういったときも Scalar::Util#blessed を利用する必要がありました。

演算子の連鎖が可能に

 python のように同じ優先順位の比較演算子、等価演算子であれば連鎖して書くことが可能になりました。
 $x < $y && $y <= $z$x < $y <= $z$x == $y && $x == $z$x == $y == $z といったふうに記述できるようになります。

 注意点がいくつかあって、まず、優先順位が違う演算子を連鎖させることは不可能です。
例えば $x < $y == $z$x < $y が先に評価され、評価した結果を $z と等価か調べる挙動となります。

 また、サブルーチンや式の結果は使いまわされます。
 例えば $x < expensive_sub() <= $zmy $tmp = expensive_sub(); $x < $tmp && $tmp <= $z と同じ挙動に、$x < $tied_scalar + 12 < $ymy $tmp = $tied_scalar + 12; $x < $tmp && $tmp <= $z と同じ挙動になります。
 なお、 tie されたスカラ変数が演算子にそのまま渡される場合は毎回 FETCH を、演算子オーバーロードされたクラスのインスタンスの変数が演算子にそのまま渡される場合は毎回演算子に対応するメソッドを呼び出します。
 例えば $x < $tied_scalar < $y を実行すると、$x < $tied_scalar && $tied_scalar < $y というふうに動作するので $tied_scalar と紐付いているFETCHメソッドは2回実行され、実行されるたびに返された値で条件式が実行されます。

Unicode 13.0 のサポート

 PerlUnicode のサポートが手厚くて恒例のという感じです。
サポートにより具体的にどういったことができるかは完全に把握できていないですが、Unicode文字名でUnicode文字を指定できるようになったり、正規表現Unicode文字特性にマッチさせることができるようになっているのを確認しています。
 例えば Unicode 13.0 では忍者の絵文字が追加されているので say "\N{NINJA}" で(OSがサポートしていれば)忍者の絵文字を標準出力に出力できたり、 "\N{NINJA}" =~ /\p{Emoji}/Unicode文字特性にマッチさせれたりといった感じです。

正規表現の中で \p{name=...} と書くことでUnicode文字を表現することが可能に

 今まで でも\N{...}Unicode文字を表現できていましたが、代わりの方法として正規表現の中なら \p{name=NAME}Unicode文字を表現可能になりました。
  \N{...}との主な違いは実行時に変数が展開されるところです。

my $name = "NINJA";
"\N{$name}";       # Unknown charname '$name'
qr/\p{name=$name}/ # \p{name=NINJA} として解釈される

 他にも細かい点で違いがあって、詳しく知りたい方はドキュメントを読むといいと思います

新しいUnicode文字特性 Identifier_Status, Identifier_Type のサポート

 これに関してはperldeltaに貼ってある unicode.org の記事が見れなかったので詳細は不明です・・・。
 分かり次第追記するかもです

*1:Perlのメソッドは実態としてはただのサブルーチンなので、パッケージ名を修飾すれば普通にサブルーチンとして呼び出すことができます

なるべく確実にメールを送信する処理を書くには

メール送信処理は単純にコードを書くだけではダメっぽいのでどうすればいいのかという話

メールが送信に失敗する状況

何らかのエラーによって送信したメールが送信者に差し戻されることをバウンスという。
バウンスにはソフト / ハードの2種類がある。

ハードバウンス

永続的なエラーによるバウンスで、そのメールアドレスには基本的に届けることができない。
例えばDNSの参照に失敗する、メールアドレスが存在しない、メールアドレスの書式が不正、受信拒否されている、など。

ソフトバウンス

一時的なエラーによるバウンス。時間をおいて再送信すれば届くことが多い。
例えばメールボックスがいっぱい、メッセージサイズが大きすぎる、サーバがダウンしている、など。

メール送信のエラー処理の書き方

つまりなるべく確実にメールを送信したいのなら、バウンスがソフトバウンスなら時間を少しおいて送信処理をリトライするような処理を書けば良い。

ソフトバウンスならエラーコードが400系、ハードバウンスなら500系になるので、1回目の送信で400系のエラーなら1, 2分間隔を空けつつ3回ほどリトライ処理を行い、
500系のバウンスの場合はリトライしても送れない可能性が高いのでなぜ送信できなかったかを確認できるようにログに書き出すなどしておく。

以下PerlEmail::Sender を使ってなるべく確実にメールを送信する処理を書いた例

use Encode qw( encode );
use Scalar::Util qw( blessed );
use Log::Minimal qw( critf );
use Email::Sender::Simple qw( sendmail );
use Email::Simple;
use Email::Sender::Transport::SMTP ();

my $transport = Email::Sender::Transport::SMTP->new(
  host          => $host,
  port          => 587,
  sasl_username => $username,
  sasl_password => $password,
);

my $email = Email::Simple->create(
  header => [
    To      => $address,
    From    => $sender_address,
    Subject => encode('MIME-Header-ISO_2022_JP', 'タイトル'),
  ],
  body => encode('ISO-2022-JP', '本文'),
);
redundanted_sendmail($email, +{ transport => $transport });

sub redundanted_sendmail {
  my ($email, $options) = @_;
  my $retry_num = 2;
  for (0 .. $retry_num) {
    eval { sendmail($email, $options) };
    if (my $e = $@) {
      die $e unless blessed($e) && $e->isa('Email::Sender::Failure');
      if ( $e->code =~ /^4[0-9]{2}$/ ) {
        if ( $_ >= $retry_num ) {
          critf "Send mail failed. code: %d %s", $e->code, $e->message;
          die $e;
        }
        sleep 60;
      }
      elsif ( $e->code =~ /^5[0-9]{2}$/ ) {
        critf "Send mail failed. code: %d %s", $e->code, $e->message;
        die $e;
      }
      else {
        die $e;
      }
    }
  }
}

しかし、エラーが返ってきても何度もメールを送信していると相手からBANされることがあったりするので、少量の送信ならば無料枠で済むSendGridの無料枠を使ったほうが良さそう。

参考

WEB+DB Press Vol.115 の Perl Hackers Hub に寄稿しました

2/22 に発売される WEB+DB Press Vol.115 の Perl Hackers Hub に「動的なモジュールロードとの付き合い方」というタイトルで寄稿させていただきました。
恐れ多くも商業誌デビューとなります。

(他記事や他の作者の方を見て、僕のような未熟な人間がこのような場にいていいのかと改めて戦慄しています・・・)

記事の内容は以下のようになっています。

  • Perlで動的なモジュールロードをする方法とハマりどころ
  • 動的なモジュールロードで実現できること
  • 動的なモジュールロードの活用
  • 動的なモジュールロードで発生する問題とその対策

過去にブログで書いた記事や、昨年11月にYAPC::Nagoya::Tiny で発表したトークを実践的に深堀りした内容ですね。

Perlではほかの言語と比べ、言語の柔軟性や文化的な要因により動的なモジュールロードが利用されることが多いと思います。
実際、拡張性や実装の手間の削減といった面で多大な恩恵もたらすので、僕も仕事や趣味でよく利用してきましたし、 CPANでも動的にロードする処理をよく見かけます。
しかし、規模が大きいプロダクトや継続的にメンテナンスされるコードでは、動的なモジュールロードが原因で保守性が低下し様々な問題が発生する・・・という経験をしてきました。
そこで、改めて動的なモジュールロードでどのようなことができるようになり、どのように活用されているかをまとめ、その上でどのような問題が起きるのか、問題に対してどのような対策をうてるのかを調査したり考えた、という内容になっています。
動的なモジュールロードはなかなか扱いが難しいので、この記事で述べたことが使いどころの指針になったり、より保守性の高いコードを書くための助けになれば幸いです。

僕の記事以外にも、競プロの過去問からアルゴリズムを学べる記事や、iOS13での新機能に関する記事、12月にリリースされたRuby2.7についての記事、さくっとゲームを作る方法を解説している記事などおもしろそうな記事がいろいろありますので、ぜひお手に取ってみてください。
よろしくお願いいたします!

今回このような貴重な機会をくださった方、記事の内容を校正してくださったりレビューしてくださった関係者の皆様方、アドバイスをくれたり応援してくださった方々にこの場を借りて御礼申し上げます。
とても嬉しかったですし、勉強になりました。
ありがとうございました。

Types::TypedCodeRef というモジュールを作りました

概要

Types::TypedCodeRef はPerlで「関数の型」をチェックする型を提供するようなモジュールです。
調べた感じ匿名サブルーチンにいい感じに引数の型とサブルーチンの返り値の型をつけてくれるようなモジュールもなさそうだったので、
AnonSub::Typed という匿名サブルーチンの引数の型と返り値の型をチェックするようなモジュールも作りました。

https://github.com/ybrliiu/p5-Types-TypedCodeRef
https://github.com/ybrliiu/p5-AnonSub-Typed

使い方

use v5.30;
use Test2::V0;
use Types::TypedCodeRef qw( TypedCodeRef );
use Types::Standard qw( Int Str );
use AnonSub::Typed qw( anon );

my $type = TypedCodeRef[ [Int, Int] => Int ];
ok $type->check(anon [Int, Int] => Int, sub { $_[0] + $_[1] });
ok !$type->check(0);
ok !$type->check([]);
ok !$type->check(sub {});

package Hoge {

    use Moo;
    use Types::Standard qw( ArrayRef Int );
    use Types::TypedCodeRef qw( TypedCodeRef );

    has event_handlers => (
        is       => 'ro',
        isa      => ArrayRef[ TypedCodeRef[ [ Int, Int ] => Int ] ],
        required => 1,
    );

}

my $hoge = Hoge->new(event_handlers => [ anon [Int, Int] => Int, sub { $_[0] + $_[1] } ]);
is $hoge->event_handlers->[0]->(12, 13), 25;

done_testing;

モチベーション

Perlでコード書いているときにもObserverパターン的なアーキテクチャを作りたくなることがあるのですが、
いちいち愚直にObserverパターンを実装するのは面倒くさいですし、かといってイベントハンドラみたいにCodeRefだけわたして雑に実装しようとすると、
イベントハンドラに登録するコールバック関数に渡す引数と関数の返り値の型がわかりにくくなり、コードの可読性が落ちるので、なんとかしたくなり実装しました。

最初は Function::Parameters や Function::Return あたりのモジュールで匿名サブルーチンの引数と返り値の型付けもできるかなーと思っていましたが、どうやら匿名サブルーチンの場合はattributeが有効にならないようだったので、(attributeの実装的にも恐らくそうなる)、匿名サブルーチンの引数と返り値の型チェックを行うモジュール AnonSub::Typed の実装を行ってから、Types::TypedCodeRef を実装しました。

実装について

AnonSub::Typed

型に関する情報もGCで一緒に管理されてほしいので、匿名サブルーチン本体をblessしたinside-outオブジェクトを返すクラスとして実装していて、それにパラメータの型や返り値の型情報を紐付けています。
クラスビルダーには Class::InsideOut というクラスビルダーを利用しています。
後は愚直に anon という AnonSub::Typedインスタンスを作る関数を提供しています。

Types::TypedCodeRef

Type::Tiny のコードを参考にしつつ、総称型のような型を作っています。
同じインターフェースの関数かどうかは最近kfly8さんが作られた Sub::Meta を利用して比較しています。
また、外部からコールバック関数を登録すれば AnonSub::Typed 以外のインスタンスでも比較できるような設計にしています。

今後の展望

AnonSub::Typed はもうやることはないかなーという感じですが、
Types::TypedCodeRef の方は AnonSub::Typed 以外を利用しているパターンでも関数の型を取得できる場合があればコールバック関数を登録しなくても比較できるようにしたいです。
後はテストとドキュメントを充実させたらCPANizeしたいと思っています。

MySQLの INFORMATION_SCHEMA.COLUMNS CHARACTER_MAXIMUM_LENGTH で得られる値についての調査

概要

諸事情によりMySQLのテーブルの文字列型カラムに入る最大値を取得したい欲求が発生し、 INFORMATION_SCHEMA データベースの COLUMNS テーブルの CHARACTER_MAXIMUM_LENGTH*1 を利用することになったので、
CHARACTER_MAXIMUM_LENGTH を参照した時、それぞれのデータ型がどのような値を返すのかのメモを残します。

まとめ

  • 文字列型の場合にのみ、そのカラムに入る文字列の長さの最大値が返ってきます。
  • サイズが指定できる文字列型(VARCHAR, VARBINARYなど)の場合、指定されたサイズが返ってきます。
  • サイズが指定できないような文字列型(TEXTなど)の場合、その型に格納できる文字列の長さの最大値が返ってきます。
  • ENUM型、SET型の場合は少し特殊です
    • ENUM型の場合、要素の中で最も長い文字列の長さを返します
    • SET型の場合、sum(各要素の文字列の長さ + 1) - 1 と言った風になります(例えば、 SUM('A', 'BB', 'CCC') となっているカラムがあれば、 8が返ってきます)
  • 文字列型以外のカラムの型の情報を見ようとすると NULL が返されます

調査内容について

  • 環境 : MySQL5.6, MySQL8.0
  • 調査のために作ったテーブル
CREATE TABLE sandbox (
  `char`          CHAR(64),
  `char_big`      CHAR(255),
  `varchar`       VARCHAR(16),
  `varchar_big`   VARCHAR(255),
  `tinytext`      TINYTEXT,
  `text`          TEXT,
  `mediumtext`    MEDIUMTEXT,
  `longtext`      LONGTEXT,
  `binary`        BINARY(32),
  `binary_big`    BINARY(255),
  `varbinary`     VARBINARY(128),
  `varbinary_big` VARBINARY(255),
  `tinyblob`      TINYBLOB,
  `blob`          BLOB,
  `mediumblob`    MEDIUMBLOB,
  `longblob`      LONGBLOB,
  `enum` ENUM(
    'hoge',
    'fugafuga',
    'piyopiyopiyo'
  ),
  `set` SET(
    'foo',
    'barbar',
    'bazbazbaz'
  ),
  `set2` SET(
    'wktk',
    'hshs',
    'blahblahblah',
    'orz'
  ),
  `date`      DATE,
  `datetime`  DATETIME,
  `timestamp` TIMESTAMP,
  `time`      TIME,
  `year`      YEAR,
  `tinyint`   TINYINT,
  `smallint`  SMALLINT,
  `mediumint` MEDIUMINT,
  `int`       INT,
  `bigint`    BIGINT,
  `decimal` DECIMAL,
  `float`   FLOAT,
  `double`  DOUBLE,
  `bit` BIT,
  `geometory`           GEOMETRY,
  `point`               POINT,
  `line_string`         LINESTRING,
  `polygon`             POLYGON,
  `geometorycollection` GEOMETRYCOLLECTION,
  `multipoint`          MULTIPOINT,
  `multilinestring`     MULTILINESTRING,
  `multipolygon`        MULTIPOLYGON
);

実行結果

SELECT COLUMN_NAME, CHARACTER_MAXIMUM_LENGTH FROM INFORMATION_SCHEMA.COLUMNS WHERE table_name = 'sandbox'; 
+---------------------+--------------------------+
| COLUMN_NAME         | CHARACTER_MAXIMUM_LENGTH |
+---------------------+--------------------------+
| char                |                       64 |
| char_big            |                      255 |
| varchar             |                       16 |
| varchar_big         |                      255 |
| tinytext            |                      255 |
| text                |                    65535 |
| mediumtext          |                 16777215 |
| longtext            |               4294967295 |
| binary              |                       32 |
| binary_big          |                      255 |
| varbinary           |                      128 |
| varbinary_big       |                      255 |
| tinyblob            |                      255 |
| blob                |                    65535 |
| mediumblob          |                 16777215 |
| longblob            |               4294967295 |
| enum                |                       12 |
| set                 |                       20 |
| set2                |                       26 |
| date                |                     NULL |
| datetime            |                     NULL |
| timestamp           |                     NULL |
| time                |                     NULL |
| year                |                     NULL |
| tinyint             |                     NULL |
| smallint            |                     NULL |
| mediumint           |                     NULL |
| int                 |                     NULL |
| bigint              |                     NULL |
| decimal             |                     NULL |
| float               |                     NULL |
| double              |                     NULL |
| bit                 |                     NULL |
| geometory           |                     NULL |
| point               |                     NULL |
| line_string         |                     NULL |
| polygon             |                     NULL |
| geometorycollection |                     NULL |
| multipoint          |                     NULL |
| multilinestring     |                     NULL |
| multipolygon        |                     NULL |
+---------------------+--------------------------+

Types::Standard で利用できる、知っておくと便利な型

概要

Perlで型チェックを行うモジュールに Type::Tiny というものがあります。
Type::Tiny には Types::Standard というライブラリがついてきて、いろいろな型が利用可能になるわけですが、
これはその Types::Standard で利用可能な、個人的に知っていると便利だったりわかりやすいコードがかけていいなーと思う型を紹介する記事です。

Enum[@list]

列挙型です。
引数として渡した文字列のリストのうち、いずれかにマッチする値だけを許容する型です。

use Test2::V0;
use Types::Standard qw( Enum );

my $constraint = Enum[qw( green blue red )];
ok $constraint->check('green');
ok !$constraint->check('hoge');

(これはほとんどの人が知ってそう)

Maybe[T]

未定義値あるいは型パラメータ(に相当するもの)として渡した型制約を満たす値を受け付ける型です。

use Test2::V0;
use Types::Standard qw( Maybe Int );

my $constraint = Maybe[Int]; # 未定義値か整数を受け付ける型制約
ok $constraint->check(1);
ok $constraint->check(undef);
ok !$constraint->check([]);

これを使うと何が嬉しいかというと、この型制約でチェックする値はnullableであるということが明示的になることです。

例えば、引数としてユーザーのIDを渡し、DBからユーザーの名前を取得するようなメソッドを書くとします。
DBには指定されたユーザーIDに対応するユーザー名がない場合があるわけですが、 Function::Return と Maybe型 で返り値のチェックをさせると、このように未定義値が文字列が返ってくるメソッドであるということが明示的になります。

use Types::Standard qw( Maybe Int );
use Function::Return;

sub get_user_name :Return(Maybe[Str]) {
  my ($self, $id) = @_;
  return $self->db->select(user => +{ id => $id });
}

Dict[ $key => $constraint, ... ]

列挙された全てのkeyと対応するvalueの型制約を満たすハッシュリファレンスのみを受け付ける型を作ることができるものです。
ネストさせることも可能です。

例えば、 idというキーがInt型の値を持ち、nameというキーがStr型の値を持つようなハッシュリファレンスのみを受け付ける型は、以下のようにして作ることができます。

use Test2::V0;
use Types::Standard qw( Dict Int Str );

my $constraint = Dict[
  id   => Int,
  name => Str,
];
ok $constraint->check(+{
  id   => 0,
  name => 'anon',
});
ok !$constraint->check(+{});
ok !$constraint->check(+{ status => 'success' });

ハッシュリファレンスで作られているデータ構造を受け渡ししているようなサブルーチンとかで使うと、どんなデータを受け渡しているのかが明示的になって読みやすいコードになります。

TypeScriptのオブジェクト型と似たようなものだと思います。

Tuple[...]

タプルです。
ちゃんと説明すると、列挙された全ての型制約を満たすような要素をもつ配列リファレンスのみを受け付ける型を作ることができるものです。
ネストさせることも可能です。

例えば、0, 1番目が数値で、2番目が文字列である配列リファレンスのみを受け付ける型は、以下のようにして作ることができます。

use Test2::V0;
use Types::Standard qw( Tuple Num Str );

my $constraint = Tuple[Num, Num, Str];
ok $constraint->check([0.4, 10, 'hoge']);
ok !$constraint->check([]);
ok !$constraint->check(['foo', 'bar', []]);

順序づけられた組であることを明示的にコードに書きたい場合や、手っ取り早くデータ構造を作りたい時などに便利なのではないでしょうか。

Optional[T]

Dict, Tuple と一緒に使う型で、省略可能な要素を作れます。

例えば先ほどのDict型のnameをキーとする組を省略可能にしたい場合は、次のようになります。

use Test2::V0;
use Types::Standard qw( Dict Optional Num Str );

my $dict = Dict[
  id   => Num,
  name => Optional[Str],
];
ok $dict->check({
  id   => 0,
  name => 'anon',
});
ok $dict->check({ id => 0 });

Tuple型の最後の要素を省略可能にしたい場合は次のようになります。

use Test2::V0;
use Types::Standard qw( Tuple Optional Num Str );

my $tuple = Tuple[Num, Num, Optional[Str]];
ok $tuple->check([0.4, 10, 'hoge']);
ok $tuple->check([0.4, 10]);
ok !$tuple->check([]);
ok !$tuple->check(['foo', 'bar', []]);

(Optional単独でも使えますが、その場合の動作は未定義のようなので使うべきではありません。)

HasMethods[@methods]

引数として渡された全てのメソッドをもつクラスのインスタンスのみ受け付ける型です。

以下のコードでは do_something メソッドをもつインスタンスのみを受け付ける型制約を作っています。

package Foo {

  sub new {
    my $class = shift;
    bless +{}, $class;
  }

  sub do_something {}

}

package Bar {

  sub new {
    my $class = shift;
    bless +{}, $class;
  }

}

use Test2::V0;
use Types::Standard qw( HasMethods );

my $constraint = HasMethods['do_something'];
ok $constraint->check(Foo->new);
ok !$constraint->check(Bar->new);

duck typing を利用してメソッドを呼び出している場所があるメソッドの引数のバリデーションに利用するなどすると、どんな引数を渡せばいいのかが明示的なコードが書けてよいです。

use Type::Params qw( compile );
use Types::Standard qw( HasMethods );

sub hoge {
  state $check = compile(HasMethods['do_something']);
  my ($foo) = $check->(@_);

  $foo->do_something();
  ...
}

ちなみに、 Types::Util の duck_type 関数で同等のことができます。
どちらを使うかは好みになるでしょうか・・・。

おわりに

これらの型を活用することでよりわかりやすいコードが書けると思うのでぜひ活用してみてください。
あと、 Types::Standard には他にも知っているとためになるような型がいろいろあるので、時間があればよく目を通しておくといいかなとおもいます。
例えばtieとかoverloadをよく使っているコードがあるのなら、tieされている値やoverloadされている値しか受け付けない型みたいなのも利用できます。

Perlにおける動的なモジュールロードのメリットとデメリットについて

概要

  • Perlにおける動的なモジュールロードのメリットとデメリットについて、そしてどういう場合に動的なモジュールロードを行うといいか、僕が考えていることを記します
  • 例のコードや具体例の説明などはPerlの文脈で行っていますが、動的なモジュールロードが可能な言語ならどの言語でも似たような利点と欠点が発生することになると思います

ご指摘などあればコメント欄で教えていただけると幸いです。

動的なモジュールロードを行うことによるメリット

アプリケーションの起動にかかる時間を短縮できる

モジュールをロードするタイミングをコードを実行する直前にまで遅らせることができるので、アプリケーションの起動にかかる時間を短くすることができます。
高速化を意識しているCPANモジュールなどで、このようなコードを見かけることが多い気がしています。

例えば Class::Accessor::Lite ではこのようなことをしています。
https://metacpan.org/release/Class-Accessor-Lite/source/lib/Class/Accessor/Lite.pm#L7

コード量を少なくできる

例えば、下記のコードのようにたくさんのモジュールを1個1個useするのは結構面倒だったりしますが、動的にモジュールをロードする場合はfor文で一括でロードできちゃったりします。

use MyApp::Foo;
use MyApp::Bar;
use MyApp::Baz;
use Module::Load qw( load ); # コアモジュールのモジュールローダーです
load "MyApp::$_" for qw( Foo Bar Baz );

また、Catalystのモジュールローダーのように*1、useなどモジュールをロードする関数を呼び出さずに、モジュールローダーを介して直接他のモジュールのメソッドを呼び出させることもできます。
この場合、長いモジュール名のモジュールをある程度短い名前で呼び出せるというメリットもあるかなと思います。

use MyApp::Model::Foo;
sub hoge {
    my $self = shift;
    MyApp::Model::Foo->do_something();
}
use Module::Load qw( load );

sub hoge {
    my $self = shift;
    # モジュールローダー(modelメソッド)は MyApp::Model::Foo をまだロードしていない場合ロードして、
    # 'MyApp::Model::Foo' という文字列を返す。 
    # -> 演算子は左側の文字列をパッケージ名としてメソッドを呼びだそうとするので、 
    # MyApp::Model::Foo->do_something() と同じ処理を行う
    $self->model('Foo')->do_something();
}

sub model {
    my ($self, $module_name) = @_;
    my $package_name = 'MyApp::Model::' . $module_name;
    load $package_name;
    die $@ if $@;
    return $package_name;
}

モジュールをロードする順番を動的に決めることができる

モジュールロードを実行時に行うと、モジュールをロードする順番を動的に決めることができたり、状況に応じて依存するモジュールを変えれたりします。

例えば(そもそもこのような設計はよくないのですが)、以下のコードのように相互に依存するモジュールがある場合、静的に互いのモジュールをロードしていると、コンパイル時に互いを再帰的にロードしようとしてしまうためか(?)、それぞれのファイルのコンパイル時に Subroutine redefined の警告が出ます。

Foo.pm
package Foo;

use strict;
use warnings;
use feature 'say';
use Bar;    

sub hoge {
    say 'hoge';
}
    
sub fuga {
    Bar::hogera();
    say 'fuga';
}

1;
Bar.pm
package Bar;

use strict;
use warnings;
use feature 'say';
use Foo;

sub piyo {
    Foo::hoge();
    say 'piyo';
}

sub hogera {
    say 'hogera';
}

1;

しかし、以下のように依存しているモジュールが必要になる直前に動的にモジュールロードを行った場合、そのような警告は出ません。

Foo.pm
package Foo;

use strict;
use warnings;
use feature 'say';
    
sub hoge {
    say 'hoge';
}
    
sub fuga {
    require Bar;
    Bar::hogera();
    say 'fuga';
}

1;
Bar.pm
package Bar;

use strict;
use warnings;
use feature 'say';

sub piyo {
    require Foo;
    Foo::hoge();
    say 'piyo';
}

sub hogera {
    say 'hogera';
}

1;

動的なモジュールロードを行うことによるデメリット

依存しているモジュールで何かエラーが起きていても、実際にロードされるまでわからない

依存しているモジュールで何かエラーが起きていても、実際にロードされるまでわからないため、アプリケーションの起動後にSyntax errorなどコンパイル時にわかるようなエラーで一部の機能が使えない・・・といった事態が発生してしまう恐れがあります。
全てのモジュールのテストが書かれていればこのようなデメリットを無視できますが、通常必ず全てのモジュールにテストが用意されていることに期待をすべきではないでしょう。

静的解析を利用したツールの恩恵にあずかれなくなる。また、読みにくいコードになる恐れがある

これはモジュールロード時にモジュール名を動的に組み立てている場合に発生する問題で、動的なモジュールロードによる問題というよりは動的にトークンを組み立てる問題と言えそうですが、動的にモジュールロードを行うときに一緒によく起きがちな問題なので取り上げます。

例えば先ほど上げたCatalyst風のモジュールローダーでモジュールをロードしている場合、静的解析ではどのようなモジュールをロードしているかはわからないため、コードジャンプやメソッド名を補完するようなツールの中には正常に動作しないものがけっこうでてきます。
そのようなツールに特別な対応させることで動作させることは可能かもしれませんが、その分の対応コストがかなりかかるでしょう。

sub hoge {
    my $self = shift;
    # modelメソッドがどういうモジュール名を返すのかは,
    # 実際にコードを実行してみないとわからない!
    $self->model('Foo')->do_something();
}

また、動的にモジュールロードされていることを知らない人がコードを読んだ場合、依存モジュールがどこで使われているのかがわかりにくくなってしまう恐れがあります。

モジュール間の依存関係がわかりにくくなってしまって、変な設計をしてしまう原因になる

  • モジュール間で相互依存が発生している場合も警告がでないので気づけなくなってしまう
  • モジュール間の依存関係を機械的に解析することが難しくなる
  • 個人的な感覚ですが、コードの途中でモジュールがロードされているより、モジュールのある一箇所で依存しているモジュールがまとめてuseされている方が依存関係がわかりやすいと思います

他にも、CHECK, INITコードブロックに書かれたコードが実行されないなどといった細かい問題などもあります。

所感

僕としては、特に長期的にメンテナンスしていく必要のあるコードや他の人が読むようなコードおいてはデメリットの方が目立ってしまうため、プロダクトではなるべく動的なモジュールロードは行わない方がよい開発体験が得られるのではないかと思っています。
動的なモジュールロードを行ってもよい場面は、個人的に使う本当に使い捨てのスクリプトを書くような場面や、CPANモジュールを作るときにアプリケーションの起動が遅くならないようにしたい場合や、Plackのようなエンジンを作るなどといった場合に限られるのではないでしょうか。