前回の(1)はこちらから。
移植時に活用すべき機能
移植方針の目処が立ったら、いよいよ実装です。Perlの言語機能やライブラリをフル活用することで、Perlらしさと実行速度を両立したコードを書けます。
(2)では、ライブラリを移植するにあたって活用すべきPerlの機能を解説します。
正規表現
Perlでは、文字列に対してメソッドを呼び出すのではなく、文字列を操作する関数を呼び出したり正規表現を使ったりして文字列を操作します。移植元の言語で文字列オブジェクトのメソッドを呼び出して文字列操作をしているときは、移植元の言語のドキュメントを読んで、どのような操作が行われているのかを把握しましょう。
たとえば、文字列の先頭から1文字ずつ読んで何か処理を行いたいとします。Rubyでは、String
クラスのeach_char
インスタンスメソッドを呼び出すことで実現できます。
Perlでは、同様の処理を次のような正規表現マッチを使って実現します。
g
フラグを付けた正規表現マッチは、リストコンテキストでは正規表現にマッチする部分文字列の配列を返します。したがって、正規表現マッチと配列を操作する関数を組み合わせることで、複雑な文字列操作を実現できます。
配列を処理する関数・ライブラリ
ある配列の各要素を加工した配列を作ったり、条件に応じて要素を取り除いた配列を作ったりしたいとします。Rubyなど多くのプログラミング言語では、配列を表すクラスのインスタンスメソッドを呼んで配列操作を行います。
Perlの配列は、Rubyなどの言語のようにインスタンスメソッドを呼び出すことができません。Perlで同様の配列操作を実現するには、for
ループで要素をpush
していく方法と、map
やgrep
といった配列を操作する組込み関数を使う方法が考えられます。
単純な処理であれば、後者のほうが処理速度の面で有利です。配列の各要素を2倍にする関数twice
を、for
ループで各要素を2倍にしてpush
する方法と、map
を使う方法とで実装した場合の速度を比較してみましょう。以下に、ベンチマークスクリプトtwice.pl
と筆者の環境(MacBook Air 2020、1.2GHz Intel Core i7、16GBRAM)でのベンチマーク結果を示します。
map
を使うほうが、for
ループでpush
していくよりも2倍近く高速に処理できていることがわかります。
map
やgrep
といった組込み関数よりも高度な処理を行いたい場合は、List::Util、List::MoreUtils、List::UtilsByといった配列操作のためのライブラリを活用してください。ライブラリの実装をシンプルに保ちつつ配列を加工できます。
移植時に注意すべきこと
せっかくライブラリをPerlに移植できても、意図どおりに動かなかったり、ライブラリの利用者を混乱させたりしてはもったいないです。ひとたびライブラリにこれらの「罠」を埋め込んでしまうと、互換性の観点から罠を修正することが困難になる場合があります。ライブラリの初期実装の段階で、できる限り罠を回避するべきです。
本節では、他言語のライブラリをPerlに移植するにあたって気を付けるべきことを説明します。
文字コード
操作する対象の文字列を、バイト列として操作したいのか、UTF-8文字列として操作したいのかに注意しましょう。場合によっては意図しない実行結果になる可能性があります。
プログラミング言語によっては、文字列を表す型とバイト列を表す型が区別されていて、両者を混合して操作するとエラーになることがあります。それに対してPerlの場合は、文字列とバイト列は明確には区別されていません。同じ文字列リテラルでも、utf8
フラグの有無によって文字列であるかバイト列であるかが変わります。また、文字列とバイト列を結合してもエラーにならず、警告が出るだけで処理は続行します。
たとえば、Perlの組込み関数length
の返す結果は、引数がバイト列かUTF-8文字列かによって変わります。
Perlの文字列の内部表現や、utf8
フラグについての詳細は、本連載の第16回「Perl内部構造の深遠に迫る」を参照してください。
YAMLライブラリの選定
YAML::Tiny、YAML::PPなどさまざまなYAMLライブラリがありますが、それぞれ処理できるYAMLの構文が異なるので注意が必要です。
YAML::Tinyはその名のとおり、シンプルな実装で必要最低限のYAMLファイルをパースすることを目標に実装されています。多くの場合はこのライブラリで事足りるでしょう。
YAML::PPはYAML 1.2に準拠することを目標として実装されています。YAML::TinyではパースできないYAMLファイルを扱う必要が出てきた場合は、YAML::PPを使うのがよいと思います。
Twitter::Textの実装にあたっては、gTLDの一覧といったデータやテストケースのYAMLファイルをパースするために、YAML::PPを使うことにしました。
関数がリファレンスを返すかどうか
Perlでは、実装する関数が複数の値を返すとき、配列そのものを返すか配列リファレンスを返すかを選ぶことができます。連想配列(ハッシュ)についても同様のことが言えます。これは多くのプログラミング言語には見られない特徴です。
どちらを選択するにせよ、ライブラリの利用者が使いやすいように、混乱を招かないように、使い分けるべきです。以降では、それぞれの場合の利点と注意すべき点を整理します。
配列そのものを返す場合
配列そのものを返す場合、map { bar($_) } func1()
のように、配列を処理する関数にそのまま引数として渡すことができます。また、関数から複数の値の組を返すときに自然な記述ができます。
一方で、配列を返す場合はライブラリ利用者がコンテキストを意識する必要があります。特に、リストコンテキストの場合は要注意です。Perlでは、配列そのものをネストさせることができません。配列をリストコンテキストで評価すると、各要素が平坦に展開されます。また、関数呼び出しの引数もリストコンテキストで評価されます。配列を返す関数を引数内で直接呼び出すと、値が展開されて、意図しない警告やバグのもととなります。
配列リファレンスを返す場合
配列リファレンスを返す場合は、利用者にコンテキストを強く意識させる必要がなくなります。関数の引数のようにリストコンテキストで評価される場所で配列リファレンスを返す関数を呼び出しても、要素が展開されることはありません。
一方で、配列を処理する関数に引数として渡す前など配列として扱いたい場合に、map { bar($_) } @{func2() }
のようにデリファレンスが必要になります。また、注意深く実装しないと空の配列リファレンスを返すつもりがundef
を返してしまい、関数のインタフェースの一貫性が失われます。
真偽値
Perlでは、次の値は偽値で、それ以外の値はすべて真値になります。
- 未定義値
undef
- 数値の
0
- 文字列の
'0'
- 空文字列
''
文字列の'0'
も偽値として扱われることに注意しましょう。デフォルトでは、真値/偽値を表すリテラルはありません。
たとえば、func(foo => 'bar')
のように、foo
という名前付き引数に文字列を渡すか、渡さない場合はデフォルト値を使うコードを書きたいとします。ここで次のようなコードを書いて$args{foo}
のデフォルト値を補完しようとすると、文字列'0'
を引数に渡したとき、$args{foo}
の値が意図せずデフォルト値になってしまいます。
次のように、exists
組込み関数を使って$args{foo}
が渡されているかどうかを判定してからデフォルト値を補完すると安全です。Perl 5.10以降をターゲットとするなら、//=
演算子を使うこともできます。
意図しない警告
use warnings
していると、文字列とundef
を連結したときなどにエラーにならず警告が出力されます。警告の数が多いと、アプリケーションのログを埋め尽くしてしまうなどノイズになります。そもそも意図しない警告が出ているのは、バグを埋め込んだ状態であるとも言えるでしょう。
移植したライブラリを使う際に、意図しない警告が出ていないかを確認しましょう。テストを実行する際にTest2::Plugin::NoWarningsというライブラリを読み込むと、警告が出たときにテストを失敗させることができます。
ただ、意図しない警告が避けられない場合もあるでしょう。その場合は、スコープを区切ってno warnings
とすることで警告を無効化できます。
まとめ
他言語で実装されたライブラリをPerlに移植する際の考え方や、気を付けるべきことについて、Twitter::Textを例に解説しました。ライブラリをPerlに移植することで、ライブラリの実装やPerlの言語機能やエコシステムについての理解を深めることができます。みなさんも、機会があればぜひ他言語のライブラリをPerlに移植してみましょう。
さて、次回の執筆者は下野寿之さんで、テーマは「表形式データを操るUNIXシェル型コマンド群」です。お楽しみに。
- 特集1
イミュータブルデータモデルで始める
実践データモデリング
業務の複雑さをシンプルに表現!
- 特集2
いまはじめるFlutter
iOS/Android両対応アプリを開発してみよう
- 特集3
作って学ぶWeb3
ブロックチェーン、スマートコントラクト、NFT