Perl Hackers Hub

第68回他言語のライブラリをPerlに移植する(2)

前回の(1)こちらから。

移植時に活用すべき機能

移植方針の目処が立ったら、いよいよ実装です。Perlの言語機能やライブラリをフル活用することで、Perlらしさと実行速度を両立したコードを書けます。

(2)では、ライブラリを移植するにあたって活用すべきPerlの機能を解説します。

正規表現

Perlでは、文字列に対してメソッドを呼び出すのではなく、文字列を操作する関数を呼び出したり正規表現を使ったりして文字列を操作します。移植元の言語で文字列オブジェクトのメソッドを呼び出して文字列操作をしているときは、移植元の言語のドキュメントを読んで、どのような操作が行われているのかを把握しましょう。

たとえば、文字列の先頭から1文字ずつ読んで何か処理を行いたいとします。Rubyでは、Stringクラスのeach_charインスタンスメソッドを呼び出すことで実現できます。

text.each_char do |ch|
    # chを使って処理する
end

Perlでは、同様の処理を次のような正規表現マッチを使って実現します。

for my $ch ($text =~ /./g) {
    # $chを使って処理する
}

gフラグを付けた正規表現マッチは、リストコンテキストでは正規表現にマッチする部分文字列の配列を返します。したがって、正規表現マッチと配列を操作する関数を組み合わせることで、複雑な文字列操作を実現できます。

my @ch = 'abcd' =~ /./g;
# => ('a', 'b', 'c', 'd')

配列を処理する関数・ライブラリ

ある配列の各要素を加工した配列を作ったり、条件に応じて要素を取り除いた配列を作ったりしたいとします。Rubyなど多くのプログラミング言語では、配列を表すクラスのインスタンスメソッドを呼んで配列操作を行います。

[1, 2, 3].map { |x| x * 2 } # => [2, 4, 6]

Perlの配列は、Rubyなどの言語のようにインスタンスメソッドを呼び出すことができません。Perlで同様の配列操作を実現するには、forループで要素をpushしていく方法と、mapgrepといった配列を操作する組込み関数を使う方法が考えられます。

単純な処理であれば、後者のほうが処理速度の面で有利です。配列の各要素を2倍にする関数twiceを、forループで各要素を2倍にしてpushする方法と、mapを使う方法とで実装した場合の速度を比較してみましょう。以下に、ベンチマークスクリプトtwice.plと筆者の環境(MacBook Air 2020、1.2GHz Intel Core i7、16GBRAM)でのベンチマーク結果を示します。

twice.pl
use Benchmark qw(:all);

sub twice_map {
    return map { $_ * 2 } @_;
}

sub twice_push {
    my @ret;
    push @ret, $_ * 2 for @_;
    return @ret;
}

my $count = 100000;
my @input = (1..10000);

cmpthese($count, {
    twice_map => sub {
        twice_map(@input);
    },
    twice_push => sub {
        twice_push(@input);
    },
});
% perl twice.pl
             Rate twice_push  twice_map
twice_push 1371/s         --       -48%
twice_map  2630/s         92%        --

mapを使うほうが、forループでpushしていくよりも2倍近く高速に処理できていることがわかります。

mapgrepといった組込み関数よりも高度な処理を行いたい場合は、List::UtilList::MoreUtilsList::UtilsByといった配列操作のためのライブラリを活用してください。ライブラリの実装をシンプルに保ちつつ配列を加工できます。

移植時に注意すべきこと

せっかくライブラリをPerlに移植できても、意図どおりに動かなかったり、ライブラリの利用者を混乱させたりしてはもったいないです。ひとたびライブラリにこれらの「罠」を埋め込んでしまうと、互換性の観点から罠を修正することが困難になる場合があります。ライブラリの初期実装の段階で、できる限り罠を回避するべきです。

本節では、他言語のライブラリをPerlに移植するにあたって気を付けるべきことを説明します。

文字コード

操作する対象の文字列を、バイト列として操作したいのか、UTF-8文字列として操作したいのかに注意しましょう。場合によっては意図しない実行結果になる可能性があります。

プログラミング言語によっては、文字列を表す型とバイト列を表す型が区別されていて、両者を混合して操作するとエラーになることがあります。それに対してPerlの場合は、文字列とバイト列は明確には区別されていません。同じ文字列リテラルでも、utf8フラグの有無によって文字列であるかバイト列であるかが変わります。また、文字列とバイト列を結合してもエラーにならず、警告が出るだけで処理は続行します。

たとえば、Perlの組込み関数lengthの返す結果は、引数がバイト列かUTF-8文字列かによって変わります。

use utf8;
my $x = '日本語'; # $xはUTF-8文字列
warn length $x; # => 3

{
    no utf8;
    my $y = '日本語'; # $yはバイト列
    warn length $y; # => 9
}

Perlの文字列の内部表現や、utf8フラグについての詳細は、本連載の第16回Perl内部構造の深遠に迫るを参照してください。

YAMLライブラリの選定

YAML::TinyYAML::PPなどさまざまなYAMLライブラリがありますが、それぞれ処理できるYAMLの構文が異なるので注意が必要です。

YAML::Tinyはその名のとおり、シンプルな実装で必要最低限のYAMLファイルをパースすることを目標に実装されています。多くの場合はこのライブラリで事足りるでしょう。

YAML::PPはYAML 1.2に準拠することを目標として実装されています。YAML::TinyではパースできないYAMLファイルを扱う必要が出てきた場合は、YAML::PPを使うのがよいと思います。

Twitter::Textの実装にあたっては、gTLDの一覧といったデータやテストケースのYAMLファイルをパースするために、YAML::PPを使うことにしました。

関数がリファレンスを返すかどうか

Perlでは、実装する関数が複数の値を返すとき、配列そのものを返すか配列リファレンスを返すかを選ぶことができます。連想配列(ハッシュ)についても同様のことが言えます。これは多くのプログラミング言語には見られない特徴です。

# 配列を返す場合
sub func1 {
    my @array;
    ...
    return @array;
}

# 配列リファレンスを返す場合
sub func2 {
    my $array = [];
    ...
    return $array;
}

どちらを選択するにせよ、ライブラリの利用者が使いやすいように、混乱を招かないように、使い分けるべきです。以降では、それぞれの場合の利点と注意すべき点を整理します。

配列そのものを返す場合

配列そのものを返す場合、map { bar($_) } func1()のように、配列を処理する関数にそのまま引数として渡すことができます。また、関数から複数の値の組を返すときに自然な記述ができます。

# baz()は値の組を返す関数
my ($foo, $bar) = baz();

一方で、配列を返す場合はライブラリ利用者がコンテキストを意識する必要があります。特に、リストコンテキストの場合は要注意です。Perlでは、配列そのものをネストさせることができません。配列をリストコンテキストで評価すると、各要素が平坦に展開されます。また、関数呼び出しの引数もリストコンテキストで評価されます。配列を返す関数を引数内で直接呼び出すと、値が展開されて、意図しない警告やバグのもととなります。

# 配列そのものをネストさせることはできない
my @arr1 = (1, 2);
my @arr2 = (0, @arr1, 3);
# => (0, 1, 2, 3)

# bar()が(1, 2)という配列を返す場合、
# func('foo', 1, 2)と等価になる
func(foo => bar());

配列リファレンスを返す場合

配列リファレンスを返す場合は、利用者にコンテキストを強く意識させる必要がなくなります。関数の引数のようにリストコンテキストで評価される場所で配列リファレンスを返す関数を呼び出しても、要素が展開されることはありません。

# 配列リファレンスはネストできる
my $arr1 = [1, 2];
my $arr2 = [0, $arr1, 3];
# => [0, [1, 2], 3];

# bar()が[1, 2]という配列リファレンスを返す場合、
# func('foo', [1, 2])と等価になる
func(foo => bar());

一方で、配列を処理する関数に引数として渡す前など配列として扱いたい場合に、map { bar($_) } @{func2() }のようにデリファレンスが必要になります。また、注意深く実装しないと空の配列リファレンスを返すつもりがundefを返してしまい、関数のインタフェースの一貫性が失われます。

sub func {
    # 配列リファレンスを初期化していない
    my $array;

    # 初期化せずにearly returnした場合、undefが返る
    return $array unless ...;
    ...
}

真偽値

Perlでは、次の値は偽値で、それ以外の値はすべて真値になります。

  • 未定義値undef
  • 数値の0
  • 文字列の'0'
  • 空文字列''

文字列の'0'も偽値として扱われることに注意しましょう。デフォルトでは、真値/偽値を表すリテラルはありません。

たとえば、func(foo => 'bar')のように、fooという名前付き引数に文字列を渡すか、渡さない場合はデフォルト値を使うコードを書きたいとします。ここで次のようなコードを書いて$args{foo}のデフォルト値を補完しようとすると、文字列'0'を引数に渡したとき、$args{foo}の値が意図せずデフォルト値になってしまいます。

sub func {
    my (%args) = @_;
    # 間違い
    $args{foo} ||= 'default value';
    ...
}

次のように、exists組込み関数を使って$args{foo}が渡されているかどうかを判定してからデフォルト値を補完すると安全です。Perl 5.10以降をターゲットとするなら、//=演算子を使うこともできます。

sub func {
    my (%args) = @_;
    # 正しい
    $args{foo} =
        exists $args{foo}
        ? $args{foo}
        : 'default value';
    # Perl 5.10以降なら次のようにも書ける
    $args{foo} //= 'default value';
    ...
}

意図しない警告

use warningsしていると、文字列とundefを連結したときなどにエラーにならず警告が出力されます。警告の数が多いと、アプリケーションのログを埋め尽くしてしまうなどノイズになります。そもそも意図しない警告が出ているのは、バグを埋め込んだ状態であるとも言えるでしょう。

移植したライブラリを使う際に、意図しない警告が出ていないかを確認しましょう。テストを実行する際にTest2::Plugin::NoWarningsというライブラリを読み込むと、警告が出たときにテストを失敗させることができます。

use Test2::V0;
use Test2::Plugin::NoWarnings;

my $x = undef;
# 警告が出るのでテストが失敗する
print $x . ' is awesome';

ただ、意図しない警告が避けられない場合もあるでしょう。その場合は、スコープを区切ってno warningsとすることで警告を無効化できます。

use warnings;

{
    no warnings 'utf8';
    # このスコープ内ではutf8に関する警告が無効化される
}

# スコープ外ではutf8に関する警告が出る

まとめ

他言語で実装されたライブラリをPerlに移植する際の考え方や、気を付けるべきことについて、Twitter::Textを例に解説しました。ライブラリをPerlに移植することで、ライブラリの実装やPerlの言語機能やエコシステムについての理解を深めることができます。みなさんも、機会があればぜひ他言語のライブラリをPerlに移植してみましょう。

さて、次回の執筆者は下野寿之さんで、テーマは「表形式データを操るUNIXシェル型コマンド群」です。お楽しみに。

WEB+DB PRESS

本誌最新号をチェック!
WEB+DB PRESS Vol.130

2022年8月24日発売
B5判/168ページ
定価1,628円
(本体1,480円+税10%)
ISBN978-4-297-13000-8

  • 特集1
    イミュータブルデータモデルで始める
    実践データモデリング

    業務の複雑さをシンプルに表現!
  • 特集2
    いまはじめるFlutter
    iOS/Android両対応アプリを開発してみよう
  • 特集3
    作って学ぶWeb3
    ブロックチェーン、スマートコントラクト、NFT

おすすめ記事

記事・ニュース一覧