Chienomi

久しぶりのPerlの使い心地は

プログラミング

Perl 5はもうすっかり廃れてしまった言語で、Perlが好きな私も最近はあまり長いプログラムは書かなくなっていた。

Perl自体が滅びたとか、恥ずかしいような風潮もあるのだけれども、LinuxerにとってはSedやAwkを発展させたさらなる便利ツールなので、Perlが活用できていないというのはコマンドライン活用が下手ということになるので、そっちのほうがだいぶ恥ずかしい話になる。

とはいえ、そういう日常的な作業としては長くても50行いかないような話であるため、プログラミングらしいプログラミングというのはPerlで行うことはごく少なくなってきている。 あえてPerlで書くことが好ましい状況というのは、$_をどれだけ多用するかという問題だし、そうなると長いコードを書くにはとても向かない状況が出来上がる。

そのため、「汚くてもいいのでインスタントに書く」ときだけPerlを使うようになるし、そもそもほとんどの場合ワンライナーで使うことになる。

こうなってくるとそれなりの長さのプログラムをPerlで書くという機会はめっきりなくなってしまい、50行を越えるプログラムを使うということは稀になってしまっていた。 50行に満たないプログラムなら、グローバル変数や雑な変数乱造は普通にあるし、サブルーチンやリファレンス、オブジェクトなどを使う機会はなかった。

だが、久しぶりに仕事で集中的にPerlを触った。 が、なかなか手ごわかった。

一番つらかったのは「デリファレンスの不明瞭さ」だ。

Rubyで言うと次のようなコード

result_list.each do |one_result|
  fname = one_result[]
  one_result.each do |match|
    linenum = match[] + 1
    match[].each do |line|
      printf '%d %s', linenum, line
      linenum += 1
    end
  end
end

Perlではこうなった。

foreach my $one_result (@{$result_list}) {
  my $fname = ${$one_result}{fname};
  foreach my $match (@{${$one_result}{result}}) {
    my $linenum = ${$match}{linenum_from} + 1;
    foreach my $line (@{${$match}{lines}}) {
      FH->printf('%d %s', $linenum, $line);
      $linenum++;
    }
  }
}

なにをどうデリファレンスするか、が複雑すぎて極めて混乱した。 単純にこれはリファレントを得たいということを実現できる仕組みになっていないのだ。 このあたりはもう少し美しい解法があってもよかったのではないかと思う。 Perl4との互換性から生まれた仕様という気もするが、このあたりの仕様がぐちゃぐちゃなPHPよりもひどい。

また、先のブログでも書いたWindows環境でのUnicodeファイル問題

open(FH, ">", "ภาษาไทยファイル名");

これが文字化けるのだ。Rubyだと問題ない。

File.open("ภาษาไทยファイル名", "w")

もっとも、RubyはMinGWで動作するし、PerlでもCygwinでは問題ないので、ここはPerlの問題とは違うかもしれない。 ただ、全体的にPerlが「アメリカ人の感覚だなぁ」と思うことはある。 優れていたはずの文字エンコーディングに対するサポートも、ちょっと甘い。

Perlの文字列操作は割と独特。 PerlはUCSを採用し、内部エンコーディングにUTF-8、内部改行文字にLFを採用している。 そして、「文字ベースで処理するにも関わらず、文字列としてバイナリがデフォルト」。

次の場合はいずれもバイナリ文字列となる。

my $str1 = "こんにちは世界";
my $str2 = <>;

open(FH, "<", "somefile");
my $str3 = <FH>;
close(FH);

次のようにすればいずれも内部文字列となる。

use utf8;
my $str1 = "こんにちは世界";

binmode(STDIN, ":utf8");
my $str2 = <>;

open(FH, "<:utf8", "somefile");
my $str3 = <FH>;
close(FH);

use open IN => ":utf8";

open(FH, "<", "somefile");
my $str4 = <FH>;
close(FH);

文字列は「バイナリ」か「内部文字列」かの2種類だ。 内部文字列はUTF-8+LFになっているので、そのまま出力すると元の文字エンコーディングに関わらずUTF-8で出力される。

open(FH, "<:encode(euc-jp):crlf", "somefile");
my $str = <FH>
close(FH)

print $str;    # 入力はEUC-JPだったけれども、「内部文字列をそのまま出力」するとUTF-8になる

Perlは文字列操作を常に正規表現で行う。 固定文字列検索は次のような具合になる。

$str =~ /\Qこ.ん.に.ち.は\E/;

あるいは

my $search_q = quotemeta("こ.ん.に.ち.は");
$str =~ /$search_q/;

きちんと内部文字列にしておかないと次のようなコードは失敗する。

$str =~ tr/A-Z/A-Z/;

Perlでは「バイナリ」「内部文字列」と言っているが、その実ASCIIとUTF-8だったりする。 内部文字列フラグがない文字列は、ASCII前提の頭で処理されるのだ。

そして、困ったことにPerlでは内部文字列フラグつきの文字列をバイナリに戻す方法がない。 文字列を内部文字列化することはできる。

use Encode 'decode';

my $str = <>;
my $internal_str = decode("UTF-8", $str);

ただし、このとき必ず変換される。 すでにUTF-8であると言えば変換されないわけではなく、 UTF-8として不正な文字は?にされてしまう。

encodeした文字列はバイナリであるが、これも変換を伴う。なので、次のコードではASCIIに変換しようとして、いずれも変換できないために出力は??となる。

use Encode qw/encode decode/;

my $str = "世界";
my $utf = decode("UTF-8", $str);
my $asc = encode("ASCII", $utf);

print $asc;

内部文字列として処理したあとURIエンコーディングを行う必要があったため、怪しげな超絶技法を駆使したりした。

{
  use bytes;
  $qword =~ s/(\W)/"%".uc(unpack("H2",))/eg;
}

だが、UCSとして文字列を扱うための機能自体は備わっている、ということを感じる。 一応固定文字列検索をindexで行う方法もある。

use utf8;
binmode(STDIN, ":utf8");

while(<>) {
  if (index(, "こんにちは") >= 0) { print; }
}

substrは実用的ではないほど使いにくいため、s///を使わざるをえないが。 文字列操作は割と悪くない。Perlの得意分野だからか。 もっといまいちな文字列の取り扱いをする言語は多く、古いわりにはまっとうに動作する。

だが、文字列を問答無用で破壊的に変更してしまうのはちょっと辛いものがある。 例えば次のようなケースでは変更前の値を変更後に使用する必要があるためコピーが必要になる。

sub get_and_save($) {
  my $qval = [0];
  my $quri = $qval;

  {
    use bytes;
    $quri =~ s/(\W)/"%".uc(unpack("H2",))/eg;
  }

  getstore("http://www.example.org/board?q=$quri", "${qval}.html"));
}

破壊的に変更したくない時は決して珍しくないので、Perlを微妙に感じる場合のひとつだ。

ちなみに、文字列の扱いに関してはRubyはCSIを採用するため「変わっている」。

文字列にはエンコーディングに関する情報を与えることができるが、 特にこの場合に変換はしない。

str.force_encoding("EUC-JP")

Ruby2.0からは文字列はdefault_externalの値のエンコーディングだとみなされる。ロケールに従うため、ここではja_JP.UTF-8を使用していることからUTF-8とみなされ、 変換しようとするとUTF-8からの変換が行われる。 次の例ではUTF-8からEUC-JPに変換している。

puts "こんにちは世界".encode("EUC-JP")

open時にPerlみたいな方法で指定することもできるようになっているが、 このときにPerlのように内部文字列の変換を行うわけではない。 次のコードではEUC-JPのファイルを読んでEUC-JPで出力する。

File.open("somefile", "r:euc-jp") {|f| puts f.gets }

EUC-JPと認識されているので、変換することはできる。

File.open("somefile", "r:euc-jp") {|f| puts f.gets.encode("UTF-8") }

あるいは内部エンコーディングを指定して自動で変換することもできる。

File.open("somefile", "r:euc-jp:utf-8") {|f| puts f.gets }

だが、Rubyは独特なので、Perlがモダンでないという話とは繋がっていない。 Rubyの文字エンコーディング関連は、日本人によるものだけに非常に楽だ。

Rubyと比べてという話にはなるのだけれど、JavaScriptやPHPとくらべても、彼らが当たり前に「よきに計らってくれる」ことを手動で面倒をみてあげないといけなかったりして、ちょっと不自由な感じがある。

文字列と数値をコンテキストで分けている、というのもちょっと微妙だ。 それだったらまだ型を指定させたほうがマシであるように感じる。 どうしても==eqの書き間違えなどは発生するし、割とデバッグもしづらい。

Perlへの批判は大部分がPerl4に起因するもので、「書き方次第」なのだが、統一感がないというか、ちょっとすっきりしない仕様というのはままある。 これはZshを使っていてbashに感じるものと似たもので、古いが故か洗練されていないというか、なにより直感に反する要素がしばしば入ってくるのがストレスだ。

まず、リファレンスを多様するのであれば他のプログラミング言語のほうが楽だ。 そして、デリファレンスをたくさん書くのであればオブジェクト指向で書くほうがずっとスマートだ。

Perlに求めていたのはそのあたりの改善だったのだが、それはPerl6によってではなく、RubyやPythonによって達成されてしまった。 だが、これらはすでに「Perlの良さ」はないため、Perlの新しいバージョンにはその改善を求めたかった。 実際にはPerl6は全く異なる言語であり、(なかなか良いように見えるが)望んだものとは少し違う。

依然としてUnixツールとしてのPerlは優れたものであり、これよりUnixツール的に優れた言語は登場していないが(Streemが実用レベルに達すればその状況は変わるかもしれない)、本格的なプログラミングにはいささか辛い部分も見え隠れする。 特に、「わかりづらくなりやすい」という点に関しては問題が大きく、さらに妙に面倒な状況が出る場合もある。

今、Perl5を使うにはいささかの割り切りが必要だ。 だが、他の大多数の言語よりも依然として良いのもまた事実。 プログラミング言語が業務的に語られることの多い昨今だが、優れた言語は優れた言語なのであるということを改めて実感させてくれる部分もあった。