なぜ bytes::length($str) はよくないのか
bytes.pm will be deprecated in Perl 5.12の話の続きです。
なぜ bytes::length()
*1を使うべきでないか。それは、一般論としてコードの意味がおかしく、また実際にバグの温床になるからです。
まず、入力されたバイト列をデコードして内部表現にし、出力の際にエンコードをするというモデルでは、文字列もその他のデータ型もまったく変わりません。たとえば、{ "foo" : 42 }
というJSONのデータをデコードして{ foo => 42 }
というPerlの内部表現にするプログラムを考てみます。この際、内部表現のサイズを直接得る関数を提供するのは妥当といえるでしょうか。
#!perl use strict; use warnings; use JSON qw(encode_json decode_json); my $input = <<'JSON'; { "foo" : 42 } JSON my $data = decode_json($input); my $length = JSON::length($data); # what? ...;
もちろん妥当ではありませんね。PerlデータのJSONバイト列へのエンコードは一意に行われるものではないので、同じデータの内部表現を表現するJSONバイト列は様々です。たとえば、API用にエンコードするのであれば余計な空白を入れませんが、設定ファイルとして書きだすのであれば整形するのが普通です。エンコードした際のサイズを得たいなら、実際にバイト列にエンコードしてからそのバイト列の長さを取ればいいのです。
一般に、エンコード・デコードを行うデータの長さがほしい時は、エンコードしてからそのバイト列の長さ取るのが妥当です。Perlでは諸事情によりたまたまbytes::length()が存在しますが、データモデルからするとこれはナンセンスなAPIといえます。
また実際このbytes::length()の仕様はバグの温床になると考えられます。たとえば以下のようなPSGIプログラムがあるとします。これは正しく動作します。
#!perl # Usgae: plackup -s CLI app.psgi use strict; use warnings; use utf8; use Encode qw(encode); use bytes (); return sub { my($env) = @_; my $hello = "こんにちは!\n"; return [ 200, [ 'Content-Lenth' => bytes::length($hello) ], [ encode 'UTF-8' => $hello ], ]; }; __END__
しかしこのPSGIアプリの出力エンコードを可変にしたいと考え、素朴に実装するとバグが発生します。
#!perl # Usgae: plackup -s CLI app.psgi --oe shift_jis use strict; use warnings; use utf8; use Encode qw(encode find_encoding); use bytes (); use Plack::Request; my %enc_map = ( 'utf-8' => find_encoding('utf-8'), 'shift_jis' => find_encoding('cp932'), 'euc-jp' => find_encoding('euc-jp'), ); return sub { my($env) = @_; my $req = Plack::Request->new($env); my $hello = "こんにちは!\n"; my $enc = $enc_map{ lc( $req->param('oe' ) || '' ) } || $enc_map{'utf-8'}; return [ 200, [ 'Content-Lenth' => bytes::length($hello) ], [ encode $enc => $hello ], ]; }; __END__
このアプリをUTF-8以外のエンコーディングで表示しようとすると、Content-Lengthと実際のボディの長さに不整合が起きます。これは、「エンコードしてからその長さを取る」という一般論に従っていれば起こりえないバグです。
もちろん潜在する危険性を承知の上で効率のためにbytes::length()を使うケースはありえます*2。しかし原則としてはbytes::length()は避けたほうがいいでしょう。