Perl の wantarray 関数で返り値の扱いを確認する2020年12月22日 22時28分

こんにちは、nanto_vi です。この記事は Perl Advent Calendar の 22 日目です。


Perl の特徴のひとつに関数が多値を返せるというのがあります。Go 言語と同様に、処理の結果とエラーの値を同時に返すことができます。

sub do_something {
    ...

    return (undef, $error) if $something_wrong;

    return ($result, undef);
}

my ($result, $error) = do_something();

上述の do_something 関数はリストを返し、その第 1 要素に処理結果を、第 2 要素にエラーの値を含んでいます。ところが、ここでうっかり、

my $result = do_something();

とリストを使わずに返り値を受け取ってしまうと、変数 $result にエラーの値が代入されてしまいます。(リストをスカラコンテキストで評価した場合、そのリストの末尾要素が返ります。)

このような事態を防ぐために、wantarray 関数を使って呼び出し側がリストコンテキストで返り値を受け取っているか確かめられます。

use Carp qw(croak);

sub do_something {
    croak 'Must handle error' unless wantarray;

    ...

    return (undef, $error) if $something_wrong;

    return ($result, undef);
}

my $result = do_something();
# => 'Must handle error' 例外が投げられる。

ここでエラーの値だけを受け取りたいときは、次のようにリストの要素として undef を書けます。

my (undef, $error) = do_something();

空リストに代入することで、返り値をすべて無視することもできます。

() = do_something();

また、defined 関数と組み合わせて呼び出し側が返り値を受け取っているか確かめられます。

sub do_another_thing {
    croak 'Must handle error' unless defined wantarray;

    return $error if $something_wrong;

    return undef;
}

do_another_thing();
# => 'Must handle error' 例外が投げられる。

my $error = do_another_thing();
# => 例外が投げられず、エラーの値が返る。

この場合、返り値を無視するには scalar 関数が使えます。

scalar do_another_thing();

Perl の Test2::V0 でオブジェクトの基底クラスを確認する2020年12月18日 00時37分

こんにちは、nanto_vi です。この記事は Perl Advent Calendar の 18 日目です。


近年、Perl でテストの記述によく使われているモジュールが Test2::V0 です。Test2::V0 について詳しくは「第51回 Test2で変わるモダンなテスト―拡張性を持ったテスティングフレームワークとTest2::V0の使い方(1):Perl Hackers Hub|gihyo.jp … 技術評論社」を参照してください。

Test2::V0 を使い、あるオブジェクトがあるクラスのインスタンスであることを確かめたいときはどうすればよいでしょうか? 例えば、オブジェクトが URI クラスのインスタンスであることを確かめようとして、次のように書くと……

# Bad
use Test2::V0;
use URI;

my $uri = URI->new('https://www.example.com/');
is $uri, object {
    prop blessed => 'URI';
};

このテストは失敗します! prop blessed の値 (Scalar::Util::blessed($uri) の返り値) は URI::https であり、URI ではないからです。(URI::https クラスは URI クラスを継承しています。)

Test2::V0 0.000138 までは、次のように書く必要がありました。

# Good
is $uri, object {
    call [ isa => 'URI' ] => T;
};

$uri->isa('URI') の返り値が真ならよいというわけです。でもパッと見ただけでは何をやっているのかわかりづらいですよね。

Test2::V0 0.000139 (日本時間で 2020 年 12 月 16 日、つまりこれを書いている 2 日前のリリースです) では、次のように書けるようになりました!

# Good
is $uri, object {
    prop isa => 'URI';
};

prop isa を使うことで、オブジェクトの基底クラスの確認をわかりやすく書けます。また、check_isa 関数も追加されました。

これらの機能は Introduce object inehritance chekcs by nanto · Pull Request #230 · Test-More/Test2-Suite で導入されました。変更をレビューし取り込んでくださった Chad Granum 氏に感謝します。

DBIx::Handler と mysql_enable_utf8 と utf8mb42013年06月10日 21時52分

Perl で MySQL サーバーに接続するとき、DBIx::Handler を使っています。

my $handler = DBIx::Handler->new(
    $dsn, $username, $password,
    {
        RaiseError        => 1,
        RootClass         => 'My::DBI',
        mysql_enable_utf8 => 1,
    },
);

しかし、これだと "𠮷" (U+20BB7、いわゆる「つちよし」) など BMP 外の文字を保存しようとしたとき、テーブルの文字コードが utf8mb4 であっても "????" (疑問符 4 文字) に化けてしまいます。

ググったところ、mysql_enable_utf8 => 1 を指定した時点で DBD::mysql が接続時の文字コードを utf8 にしてしまうのが問題だそうです。(cf. おそらくはそれさえも平凡な日々: DBD::mysqlでmysql_enable_utf8しつつutf8mb4使いたいとき)

接続時に文字コードを utf8mb4 に指定しなおせばいいとのことですが、DBIx::Handler ではどうするのだろうとソースコードを眺めていたところ、new メソッドの第 5 引数の存在を知りました。

my $handler = DBIx::Handler->new(
    $dsn, $username, $password,
    {
        RaiseError        => 1,
        RootClass         => 'My::DBI',
        mysql_enable_utf8 => 1,
    },
    {
        on_connect_do     => ['SET NAMES utf8mb4'],
    },
);

これで BMP 外の文字も文字化けせず保存・取得できるようになったのですが、この第 5 引数は (DBIx::Handler 0.07 時点では) 文書化されていないので、使っていいものか心配です。

Callbacks を使っても期待通り動作するようなので、そちらのほうがよかったりするのでしょうか。

my $handler = DBIx::Handler->new(
    $dsn, $username, $password,
    {
        RaiseError        => 1,
        RootClass         => 'My::DBI',
        mysql_enable_utf8 => 1,
        Callbacks         => {
            connected => sub {
                $_[0]->do('SET NAMES utf8mb4');
                return;
            },
        },
    },
);

(SET NAMES をつかってはいけないという話もありますが、追いきれていません。)

Template::Plugin::JSON::Escape 公開2011年06月15日 20時57分

Template::Plugin::JSON::Escape という Perl モジュールを公開しました。

Template Toolkit を使って HTML を出力するとき、HTML 中に JSON を埋め込みたいことがあります。そのためのモジュールとして Template::Plugin::JSON が存在するのですが、これは小なり記号 (<) などの文字を素通しするため、JSON の内容によっては不正な HTML が出力されてしまいます。

Template::Plugin::JSON::Escape は一部記号を \uxxxx の形式にエスケープするので、JSON の内容にかかわらず出力する HTML を妥当に保てます (もちろん、JSON 出力部以外が妥当な HTML を出力するのであれば、です。なお、SGML のコメント内に JSON を埋め込む場合はこの限りではありません)。副次作用として、XSS の防止にも一定の効果を期待できます。

また、Template::Plugin::JSON 0.06 は Moose を使っていますが、私が普段 Perl を使っている環境には Moose が入っていません。そのためだけに Moose を導入するのは難しいので、Moose を使用しないモジュールが求められていたというのもあります。

関連する話題

Perl の URI モジュールにおける文字列とオクテット列の扱いに関して2011年05月27日 17時28分

Perl の URI モジュールには ASCII 外の文字の扱いに関して欠陥がある。ここでは Perl における URI の扱いについて述べ、URI モジュールの修正方針を提案する。

この記事で取り上げた問題に関して主たる部分は URI.pm 1.59 で (この提案とは違った形で) 修正済みであり、この提案は obsolete です。

用語の定義

URI
RFC 3986 (日本語訳) で定義される識別子。
URI.pm
Perl の URI モジュール。ここでは、ファイルとしての URI.pm だけでなく、URI パッケージ下に含まれるコード全般を指す。断りがない限りバージョン 1.58 に基づく。
文字列
UTF-8 文字列と Latin-1 文字列のいずれか。
UTF-8 文字列
Perl の文字列値で、UTF8 フラグが立っているもの。
Latin-1 文字列
Perl の文字列値で、UTF8 フラグが立っておらず、文字の並びとして扱われることを期待されているもの。
オクテット列
Perl の文字列値で、UTF8 フラグが立っておらず、バイトの並びとして扱われることを期待されているもの。Latin-1 文字列とオクテット列の機械的な判別は不可能。

発端となった問題

URI.pm のメソッドに UTF-8 文字列とオクテット列を同時に渡した場合、期待した出力が得られないことがある。特に Perl 5.10 以下では、use utf8 環境下で bareword に UTF8 フラグが立つため、この問題が発生しやすい。

#!/usr/bin/perl5.8.8
use strict;
use warnings;
use utf8;
use URI;
use Encode qw/encode_utf8/;

my $uri = URI->new('http://example.org/');
$uri->query_form( foo => encode_utf8('字') );
print $uri->query, "\n";
# actual:   "foo=%C3%A5%C2%AD%C2%97"
# expected: "foo=%E5%AD%97"

Perl において URI はどう扱われるべきか

URI は US-ASCII の範囲内の文字で構成されるが、特定の文字符号化方式を仮定しない。URI には任意の文字符号化方式で符号化されたオクテットの並びが (パーセントエンコードされて) 含まれうる。たとえば、二つの URI http://ja.wikipedia.org/wiki/%E3%81%AF%E3%81%A6%E3%81%AAhttp://d.hatena.ne.jp/keyword/%A4%CF%A4%C6%A4%CA において、パーセントエンコードされた部分はいずれも「はてな」という文字の並びを表すが、前者は UTF-8、後者は EUC-JP で符号化され、パーセントエンコードされたものである。

それゆえ、Perl において URI はオクテット列として扱われるべきである。そのオクテット列がどの文字符号化方式を用いて解釈されるかは、URI とは別の層の問題である。

URI.pm をどう修正すべきか

現在、URI.pm では入力を文字列として扱うものとオクテット列として扱うものが混在している。たとえば、host メソッドの引数に渡した値は UTF-8 文字列または Latin-1 文字列として解釈され、query メソッドの引数に渡した値はオクテット列であるかのように扱われる。

ここでは、前節の主張に従い、URI.pm に関する入出力はすべてオクテット列として扱うことを提案する。IRI に関するものを除き、URI.pm の各メソッドに渡された引数はすべてオクテット列とみなされ、返り値はオクテット列となるべきである。

引数に UTF-8 文字列が渡された場合は、それを UTF-8 で符号化されたオクテット列として扱うのがよいと考える。HTTP::Messagecontent メソッドのように UTF-8 文字列を渡されたらエラーとするのもひとつの手ではあるが、現在の URI.pm との互換性を大きく損ねないためにはエラーにしないほうがよいと判断する。

国際化ドメイン名の扱い

URI.pm の入出力をオクテット列とみなすとき、問題となるのは国際化ドメイン名の扱いである。本来なら ASCII 外の文字を含めるのに URI::IRI のような専用のモジュールを使うべきだろうが、現在の URI.pm との互換性を損ねないためには、URI->new(...)$uri->host(...) で ASCII 外の文字を指定できるのが好ましい。

あくまで入力はオクテット列とみなすという方針を貫きつつ国際化ドメイン名を受け付けるため、URI のホスト部に関してはオクテット列を UTF-8 で符号化されたものとみなし、UTF-8 でデコードした文字列を Punycode 符号化して出力のホスト部とする。

URI.pm の互換性の確保

ここで提案する修正により、URI.pm は後方互換性を失う。特にホスト部に関しては、これまで Latin-1 文字列として扱われてきたものが、UTF-8 で符号化されたオクテット列として扱われ、まったく違った出力を得ることになる。

そこで、過去の挙動に戻せるよう、$URI::COERCE_OCTETS 変数を設ける。この変数のデフォルト値は真であるが、値が偽なら以前と同様に入力をオクテット列とみなさず処理を行う。

修正の段取り

以上の提案を実装すべく、現在 github の nanto/uri レポジトリの coerce_octets ブランチで作業中である。さしあたってどのような出力を期待しているのか明らかにするため、ASCII 外の文字を含む入出力に関するテストを書いた。実装が一段落したら URI.pm に対するパッチとして提出するつもりである。

#43859: should be _utf8_off -ed raw data before URI encoding修正の URL を添えて返信した。