そのうちPerlに入るかもしれないcase文Syntax::Keyword::Matchの紹介
こんにちは。
id:anatofuzです。
これはPerl Advent Calendar 2024の15日目の記事です。
昨日は
id:mackee_wさんによるaquaでperlを入れて使えるようになりました でした。relocatable-perl自分は低スペックのPCでとりあえずPerl動かしたいときによく使っています。
さて、PerlはC言語からの影響が強い言語ですが、C言語にあってPerlにない、というよりあったが使われてなかった機能にcase文があります。 かつてはPerlのコアにgiven-whenと呼ばれるcase文の機能がPerl5.10.1から導入されていました。
use v5.10.1; given ($var) { when (/^abc/) { $abc = 1 } when (/^def/) { $def = 1 } when (/^xyz/) { $xyz = 1 } default { $nothing = 1 } }
これはgivenの後ろに評価したい変数をいれ、whenキーワードでいわゆるcaseに相当するものを書くスタイルです。
一見すると良さそうな機能なのですが、givenで値をスマートマッチ演算子(~~)を使って評価するという特徴があります。
このスマートマッチ演算子が曲者で、いくつかの問題を抱えていることから5.18で非推奨となりました。
http://perldoc.jp/docs/perl/5.18.0/perl5180delta.pod#The32smartmatch32family32of32features32are32now32experimental
長らく非推奨という形だったのですが、これを受けて5.41.3(Perlの開発用バージョン)でついにスマートマッチ演算子とgiven-whenが削除されました。 次のリリース予定の安定版(Perl5.42またはPerl42)でもスマートマッチ演算子とgiven-whenは削除されてリリースとなります。
ということでコア機能からはcase文は消されたのですが、しかし全ての計算をif~elsifで書くのはちょっとだるい、というときが存在します。我々はcase文がやはり欲しくなるときがあるわけです。
さて、最近のCPANモジュールにはSyntax::Keyword名前空間のモジュールがいくつか存在します。
これはPaul Evans先生がPerlのsyntax plugin 機能を利用し構文の拡張を検証しているモジュール郡です。
PaulEvansはPerlのコア開発者でもあるので、Syntax::Keywordで成果がでた言語機能がPerl本体に取り込まれるというのが最近のムーブメントとなっています。具体的には5.36より導入されたdeferはSyntax::Keyword::Deferの開発内容が元になっています。このためSyntax::Keyword名前空間のモジュールとPerlコアに最近入っている新機能は内容がコンパチであるので、Feature::Compat名前空間のモジュールを利用するとPerlバージョンに応じてコアかSynrax::Keywordかどちらかで処理を実行するという互換性に強いPerlアプリケーションを書くことができます。例えばdeferはFeature::Compat::Deferを使うとdeferがfeatureにある場合はfeature, ない場合はSyntax::Keywordなモジュールが使われるので、まずアプリケーション側をdefer対応してからPerlのバージョンアップ、というのが比較的スムーズに行えます。
さて、given-whenは消えてしまったわけですが、なんと今CPANにはSyntax::Keyword::Matchモジュールが公開されています。もちろんこれはPaulEvansによって作られた新しいcase文の検証実装です。
use v5.16; use Syntax::Keyword::Match; my $n = ...; match($n : ==) { case(1) { say "It's one" } case(2) { say "It's two" } case(3) { say "It's three" } case(4), case(5) { say "It's four or five" } case if($n < 10) { say "It's less than ten" } default { say "It's something else" } }
主な特徴はmatchキーワードの後ろに評価したい変数と演算子を指定します。
各case文はその演算子での比較先を書くことができます。この例では$nを数字として比較演算しているわけですね。
各caseブロックはgolangと同様に自動でbreakされます。cと違い明示的に書かなくても{}のブロック内で処理を完結できるので直感的ですね。複数の評価値で実行したいブロックをまとめたい場合は,で繋いで書くことで実装できます。
ここまでだと他の言語のcase文と同じなのですが、おもしろポイントとしてcaseの後ろにifを書くことで別の評価を行うことができます。上の例ではcase if ($n < 10)としていて、ここだけ大小演算が行われるわけですね。
他にはmatch構文のスコープだけで有効な変数も定義できます。例えばこの例では$xはcase文の中だけ使うことができる変数です。
match( my $x = some_function_call() : == ) { case ... }
さてこう見ると結構使えそうな感じがありますね。実はこの前のISUCON14ではこっそりこのcase文を使ってみています。 一応導入前にif-elsifとのベンチマークの比較をしたのですが、ほぼ等価、またはcase文の方が多少早いという結果になったのでパフォーマンスも非常に良いです。 というわけである程度のアプリケーションでも実際にキビキビ動作するcase文、ぜひ使ってみてはどうでしょうか。
明日は
id:karupaneruraさんでTBDです。お楽しみに!
PerlにおけるGraphQLライブラリのまとめ
リッチなフロントエンドを開発しているとGraphQLを使いたくなるときがあり、さらにそのバックエンドをPerlで実装したくなるときもあるでしょう。
このエントリではPerlでGraphQL開発をしたい時に使える2023/02/24現在のPerlのGraphQL周りのライブラリ事情についてまとめます。
GraphQLフレームワーク
graphql-perl
PerlでCPANモジュールを使ってGraphQLバックエンドを実装したいとなった場合に現状コレ一択。
PerlCareersの支援で資金援助を受けて実装されている。
世界観としてはThis module is a port of the GraphQL reference implementation, graphql-js, to Perl 5.と言っているが、graphql-jsの忠実な移植ではなく、限りなく処理を独自実装しているのが特徴。
MooやType::TinyなどのモダンPerl的なライブラリをふんだんに使っていたり、GraphQLのパースをPegex::Parserを使い正規表現を用いて行っている。
やや実装に難がありパフォーマンスに問題が発生しがちな傾向にある。
khrt/graphql-perl
graphql-jsの忠実な移植プロダクト。 残念ながら開発が途中で止まっていそう。
stevan/p5-Graph-QL
khrt/graphql-perlと同様にgraphql-jsの忠実な移植。
開発が5年で止まって入るが、比較的実装されている。
パーサーライブラリ
GraphQL::Language::Parser
前述のgraphql-perlの一部分
Parser::GraphQL::XS
graphql公式のリポジトリにあるC++で実装されたGraphQLパーサー( https://github.com/graphql/libgraphqlparser )のXSバインダ。
ただしParser::GraphQL::XSにはlibgraphqlparserをインストールするみたいなおもてなしが無いことから、このモジュールをインストールする際は自前でlibgraphqlparserをインストールする必要がある。
DataLoader
GraphQLでクエリに対するSQLをいい感じにまとめる時に使われる機能であるDataLoaderの参考実装。 Mojoliciousが動いていることが前提となっているため、Mojoliciousを使っていないwebアプリケーションには直接組み込めない
というわけで基本graphql-perlを使えば良いのだけど、graphql-jsの素朴な移植もほしいですね。。。
Perlでスマートに配列からランダムで特定個数の値を取得する
TL;DR
use List::Util qw(sample); my @array = (1..100); # 3個ランダムでとりだす my @random_pickup = sample 3, @array;
人間生きていると配列の中身からランダムにn個取得したいときがあります。Perl書いているときもありますね。
よくある方法は次のような感じでしょうか
my @array = (1..100); my @random_array = shuffle @array ; my @random_pickup = splice @random_array, 0, 3;
List::Utilのshuffleを使い配列をランダムにばらしたあとに、標準関数のspliceを使い特定の数の値を取り出してきます。
これだと@random_pickupには3つのランダムな数が入る訳ですね。
ただしこの方法だと必ず全件をshuffleしないといけないため、巨大な配列を対象にする場合はそこそこコストが掛かります。
他には結局@random_pickupが必要な場合は、中間変数の@random_arrayが不必要ですね。
(最も、工夫すれば中間変数を作らずにかけますが...)
そこでオススメなのがList::Utilのsample関数です。1.54から同梱されているので、比較的最近のList::Utilになら入っています。 ちなみにList::MoreUtilsにも入っています。
書き方は簡単で以下の感じです。
my @items = sample $count, @values; # sample {取得したい数}、 {リスト}
$countが@valuesの配列長より大きい場合は、shuffleと同じ挙動になります。
近年のList::UtilはXS(C言語実装)も同梱されており、sampleも見てみるとC言語レベルで特定個数のランダム取得をしてくれています。 このため、コスト的にもshuffleしてからspliceなどで取得するより最適化されておりオススメです。 metacpan.org
2022年、CPAN(Perlの)モジュールのメンテナを引き継ぐ活動を始めた件
これははてなエンジニアアドベントカレンダー2022 42日目の記事です。
昨日は
id:k-murakami0609 さんの 過去に所属してたチームに転生したら導入したいもの でした。
はてなのノベルチームで日常的に使っている便利グッズ最高ですね!! みなさんもノベルチームにjoinして体験してください!!!
さて今回は2022年にぼちぼち始めたCPANモジュールのメンテナを引き継ぐ活動についてお話しようかなと思います。
CPANモジュール
CPANモジュールとはご存知プログラミング言語Perlのモジュールシステムのことです。
Perlインタプリタに付随しているコアモジュールも含めて、PerlではCPANと呼ばれるアーカイブにモジュールがアップロードされ、cpanmやcpmなどのツールを通してインストールし利用する世界観になっています。
TeXのモジュールアーカイブのCTANに影響されて作成されたと言われていることからも分かる通り、CPANは1995年から存在しており、歴史的にも古いシステムになっています。
そんなCPANには大小様々なモジュールがアップロードされています。特に有名どころではAcme名前空間でしょうか。
他の言語に無い特徴ですが、PerlのモジュールではAcme::と名前がつくモジュールはジョークモジュールとなっていて、様々なおもしろモジュールがアップロードされています。(どういう物があるか知りたい方はAcme大全がオススメです)
CPANモジュールの今
そんなCPANモジュールですが、Perl使用者の人口減に伴って、新規モジュール開発及びモジュールのメンテナが減少傾向にあります。
最近ではElasticsearchの公式クライアントライブラリのSearch::ElasticsearchもElasticsearch8を持ってPerlクライアントの提供を終了することを発表したりしています。
日本でもPerlを書かれていたエンジニアの方々が、別の言語にコミットすることになったり、また多忙でPerlを書く時間が取れなかったりと、様々な理由でメンテナが減少しています。 使わないモジュールのメンテナンスをし続けるのはなかなかしんどいですよね。
Perlではここ数年@INCがカレントディレクトリから削除されたことなどもあるため、継続してメンテナンスをしていくのは後方互換性が強いPerlといえども必要そうです。
とはいえ、はてな社にはまだPerlで元気に動いているPerlプロダクトがありますし、何より
id:anatofuzはPerlが好きなので、できればモジュールのメンテは継続していきたいです。
他にはCPANモジュールではないですが、Perlの公式Dockerイメージの管理もごく数人で行っているので、こういったところにもコミットしたいなと思っていました。
CPANモジュールのメンテに関わる活動
そこで去年はパッチを当てたいOSSを中心にメンテナを引き継ぐ活動をしました。 そこまで件数が多いわけではなかったですが、声を書けながらメンテ権を頂いています。
@cho45 こんにちは! はてなでPerl書いているAnaTofuZです。いくつか社内でcho45さんが作成された/メンテナンスされているCPANモジュールを利用させていただいているのでメンテに加わりたいです。社内にも他にメンテしたい方がいるので、まずは自分にメンテ権限いただきたいので相談させてください!
— 八雲アナグラ (@AnaTofuZ) 2022年6月9日
溜まっていたPRの解決や、新規PRのレビュー等に参加することで、継続してメンテナンスを行っています。
直近ではTest::WWW::Stubのメンテに関わりました。
モジュール以外ではdocker-perlやOpenAPIのスキーマからクライアントコードを自動生成するOpenAPI GeneratorのPerlクライアントもPRを送っています。
CIの修正なども
他にはPerlモジュールは、日本でよく使われるCPANモジュールオーケストレーションツールのMinillaが標準でTravis CI用の設定ファイルを出力していたことから、Travis CIでCIを回しているものが多かったです。
しかしTravis CIは近年では動かなかったりするので、これをGitHub actionsに移動するなどの活動もメンテ権をもらうついでにシュッと進めています。
CPANモジュールのメンテを引き継ぐ方法
CPANモジュールのメンテはGitHub等のリポジトリを引き継ぐ + CPANモジュールのcomaintに登録してもらうことで引き継ぐことができます。
この部分はオリジナルの作者の方にやっていただく必要があるため、コミュニケーションが必要です。 権限を付与されてからは自分のCPANモジュールをメンテするように操作することが可能です。
ただし引き継がれたCPANモジュールをアップロードする際はx_authorityという権限に気をつける必要があります。
※追記
id:shoichikaji さんより最近はいらないとの情報を得ました!
メンテありがとうございます!
— Shoichi Kaji (@shoichikaji) 2023年1月11日
ところで、x_authorityに関しては、もうPAUSEが勝手にやってくれるので設定しなくてもOKだと思います。https://t.co/uG4anMFjZl
※追記ここまで
新しいモジュールを作りつつメンテもしていく
新しいモジュールを作るのも楽しいですが、既存のモジュールのメンテをするのも盆栽をするようで楽しいです。 また偉大な先人のコードに自分も関わることができ、広く使われていく体験もできるので、ぜひPerlに興味がある方はCPANモジュールなどに関わっていきましょう!!!
明日のはてなエンジニアアドベントカレンダーは同じチームの
id:deflis55さんです! お楽しみに!
git.ioは非推奨(read only)になってしまったので注意する必要がありそう
表題が全てシリーズです。(シリーズとは)
完全にノーマークだったのですが、GitHubが提供していた短縮URLサービスのgit.ioが4月27日を持ってread onlyに移行しています。
2022-04-27 Update: While the git.io url redirection service is read-only and use of the service is limited, we have received feedback from developers and academic researchers who have published git.io links in print documentation and research papers. In order to preserve the integrity of these historical documents, we have decided to archive the current git.io links in a new read-only service that will allow us to serve redirects for those links longer term.
As we continue our analysis, we may remove individual links that point to spammy, malicious or 404 links. Our goal is to not break links relied on for legitimate use, especially by the academic community, while preserving the security of developers on GitHub.
That said, we still encourage users to make use of one of the many URL shortening services available with greater functionality than the git.io service provided. GitHub support will not be able to update or edit redirection records served by the git.io archive service.
もともとは4月29日に全てのリダイレクトを止める方針だったようですが、論文等ですでに利用されていることもあり、リダイレクトは止めず、新たに追加/編集等ができない読み取り専用に移行したようです。
(これはgit.ioがスパムなどの目的で使われてしまっており、そのメンテナンスコストを鑑みての判断らしいです。)
read-onlyになったのでまだ使用できるとはいえ、非推奨になってしまったので、新たなURLに移行するのが良さそうです。
Perlの人むけ情報
Perlの人向け情報としては、cpmをcpmを使ってインストールする方法が、以前はgit.ioを使ってインストールする方法が推奨されており、この問題で推奨インストール先のURLが変更されているので注意が必要です。
これは作者の
id:shoichikajiさんからもCHANGELOGで変更が呼びかけられています。
差分は次のような感じです。
旧版
$curl -fsSL https://git.io/cpm | perl - install -g --with-develop --with-recommends --show-build-log-on-failure
移行版
$curl -fsSL https://raw.githubusercontent.com/skaji/cpm/main/cpm | perl - install -g --with-develop --with-recommends --show-build-log-on-failure
ということで気づきベースで変更していくのが良さそうです。git.ioがこんな状況になってたの知らなかった....。
いい感じにuseするのを助けてくれるApp::perlimportsのご紹介
これはPerlアドベントカレンダー2021の12日目の記事です。
昨日はshogo82148さんで、Perl 5.35.5 の iterating over multiple values at a time を先取りでした。
今回はPerlでもいい感じのモジュールのImportをしてくれるApp::perlimportsの紹介です。
App::perlImports
このモジュールはThe Perl and Raku Conference 2021で作者本人からトークされていて、スライド及び動画が上がっています。
スライドはGitHub上にHTMLがあるので手で落とす必要があるようです...。(GitHubPagesは特に使っていない模様)
curl https://raw.githubusercontent.com/oalders/presentations/main/slides/6-perlimports/remark.html -o perlimports.html && open perlimports.html
「Where Did That Symbol Come From?」とトークタイトルが示す通り、Perlのシンボルがどこからimportされたかを明確にしたいというのが実装の動機になっているようです。合わせて動き自体はgolangでimportしているモジュールをいい感じに管理してくれるgoimportsを参考にしているようです。
このモジュールについて説明するにはまず、Perlのモジュールロードがどのように行われているかを確認する必要があります。ざっと見てみましょう。
Perlのモジュールロード
Perlはモジュールをロードする方法に主にuseを使った方法とrequireを使った方法があります。*1
歴史的経緯でrequireの方が先に出ていて、 Perl5でuseが導入されています。
requireはモジュールロード以外に外部ファイルの読み込みにも使われます。
近年はuseを使ってモジュールロードをするのが一般的です。
例えば、PerlにデフォルトでバンドルされているHTTPクライアントのHTTP::Tinyを使う場合は次のようにロードします。
use HTTP::Tiny; my $ua = HTTP::Tiny->new();
ロードタイミング
Perlは動的言語でありますが、useを使ったモジュールロードの処理は、該当のPerlコードをPerlインタプリタが読み込む、コンパイルタイムに行われます。
例えば存在しないHogeモジュールをロードしようとすると、次のようにprintが走ることはなくエラーで死にます。
#!/usr/bin/env perl use strict; use warnings; print "hello"; use Hoge;
❯ perl hoge.pl Can't locate Hoge.pm in @INC (you may need to install the Hoge module) (@INC contains: /Users/anatofuz/.plenv/versions/5.32.0/lib/perl5/site_perl/5.32.0/darwin-2level /Users/anatofuz/.plenv/versions/5.32.0/lib/perl5/site_perl/5.32.0 /Users/anatofuz/.plenv/versions/5.32.0/lib/perl5/5.32.0/darwin-2level /Users/anatofuz/.plenv/versions/5.32.0/lib/perl5/5.32.0) at hoge.pl line 2. BEGIN failed--compilation aborted at hoge.pl line 2.
対して、各関数呼び出しなどの処理は実行時に解決されます。 そのためuseを忘れても、対象のメソッドを実行しない限りは処理が止まることはありません。 例えば次のコードは、HTTP::Tinyをuseしていませんが、呼び出す前にdie(Perlでのエラー終了)をして処理を止めるので、ロードエラーは発生しません。
#!/usr/bin/env perl use strict; use warnings; print "hello\n"; die; my $ua = HTTP::Tiny->new(); $ua->get('https://example.com');
perl hoge.pl
hello
Died at hoge.pl line 6.
useの処理
ではuseは何をしているのでしょうか。Perldoc.jpを見るとこう書かれています。
指定したモジュールから、現在のパッケージにさまざまな内容をインポートします; 多くは、パッケージのサブルーチン名や、変数名に別名を付けることで、 実現されています。 これは、以下は等価ですが
BEGIN { require Module; Module->import( LIST ); }
BEGIN { }はBEGIN ブロックと呼ばれるもので*2、コンパイルタイムに実行させるという処理です。
中ではrequireを使い、モジュールを読み込んだ後に、そのモジュールにあるimportメソッドを実行しています。
つまり、前述したPerlのモジュールロードのuseとrequireを比較すると次のような違いがあることがわかります。
| 項目 | use | require |
|---|---|---|
| ロードタイミング | コンパイルタイム | 実行時 |
| 副作用 | importを実行する | 読み込みのみ |
整理したところで、このimportとは一体何でしょうか。
一般的には、これはモジュールで定義しているメソッドを、呼び出し元のコードで定義したかのように使うために実行されるものです。
詳しく見てみましょう。
import
Perlでは特定のモジュールをuseすると使えるようになるメソッドがいくつか存在します。
例えばコアモジュールのJSONを扱うライブラリであるJSON::PPをuseすると、任意のPerlのハッシュをJSON文字列化できるencode_jsonが使えるようになります。
use strict; use warnings; use JSON::PP; print encode_json({hoge => 'hello', isBool => \0});
encode_jsonはJSON::PPで定義されているメソッドなので、通常はJSON::PP->encode_jsonのように使う必要があります。
importは特定のメソッドを、useしたタイミングで通常の関数のように呼び出せるようによしなにやってくれる処理になっています。
この処理のことをPerlでは関数のエクスポートなどと読んでいます。
近年ではExporterモジュールが行っています。
あるモジュールで、Exporterモジュールを継承、もしくはuseすると、そのモジュール内で自動的に関数のエクスポート処理をimport関数として登録してくれます。
あくまでExporterを使った場合なので、独自にimport関数を定義することも可能です。
その場合は通常の関数のように呼び出せるようによしなにやってくれる処理以外の処理をuse時に行うように差し込むことが可能となります。
例えばClass::Accessor::LiteはimportをExport以外の目的に使っており、オブジェクトのアクセサを生成する処理を行っています。
use Class::Accessor::Lite ( new => 1, rw => [ qw(foo bar) ], ro => [ qw(baz) ], wo => [ qw(hoge) ], );
Perlでモジュールをuseする際に、exportしたい関数名の指定ではないような引数を渡しているモジュールは、ほぼ独自にimportを書いていると考えても良いでしょう。
use時に差し込まれるモノたち
Exporterを使っているモジュールがuseされたときに差し込むものは、モジュール側で定義されている@EXPORT配列に積んだシンボルになっています。((ここでのシンボルはPerlのメソッド(関数、サブルーチン)の他に、スカラ変数や配列も含みますが、Exporterを使う上ではメソッド以外のシンボルを使うと問題が発生しがちなので、基本はメソッドのことです
))
例えば、Hoge::Firstモジュールをuseしたときに、hogeメソッドを差し込みたい場合は、次の様に定義します。
package Hoge::First; use strict; use warnings; use feature qw/say/; use Exporter 'import'; our @EXPORT = (qw/hoge/); #サブルーチンの名前を文字列で配列の中にいれるとEXPORTされる sub hoge { say 'this is first!'; } 1;
Perlは@EXPORTに積まれているオブジェクトをすべてロードします。
このためuseしているモジュールが、内部で@EXPORTに何かしら詰めていた場合、予期せぬシンボルをロードする可能性があります。
また、あるPerlファイルの中で出てきているメソッドが、一体どのモジュールから提供されているか、はたまたデフォルトのメソッドであるかの判断が難しくなってしまいます。
使用しているメソッドが、どのモジュールから提供されているのが解りづらいのは、リファクタリングの結果すでにあるモジュールが提供しているメソッドを使わなくなったのに、useし続けてしまうモジュールが出たり、逆にプログラミングしている上で使えるだろうと思って書いたメソッドが、実はuseしないといけなかったなどの問題を誘発しがちです。
また、同じ名前のオブジェクトをロードしてしまうと、呼び出し順序によって使われる関数が異なってしまいます。
例えばHoge::FirstとHoge::SecondモジュールでそれぞれhogeをEXPORTしてみます。
package Hoge::First; use strict; use warnings; use feature qw/say/; use Exporter 'import'; our @EXPORT = (qw/hoge/); sub hoge { say 'this is first!'; } 1;
package Hoge::Second; use strict; use warnings; use feature qw/say/; use Exporter 'import'; our @EXPORT = (qw/hoge/); sub hoge { say 'this is second!'; } 1;
#!/usr/bin/env perl # main.pl use strict; use warnings; use Hoge::First; use Hoge::Second; hoge();
実行すると、最後にロードされたSecondが出てきます。
❯ perl -Ilib hoge.pl this is second!
@EXPORTはこのような思いがけない処理が走る可能性があるので、Exporterモジュールのコメントにも「とりあえずEXPORTするのはやめよう」といったことが書かれています。
pointoht.ti-da.net
さらにはPerlのLinterであるPerl::Criticにも@EXPORTを警告するルールが追加されています。
とはいえ関数のエクスポートは便利なので、それはそれで使用したいです。
Perlでは@EXPORTの他に@EXPORT_OKがあります。これはデフォルトではすべてをロードせず、use時にエクスポートしたい関数名を書くことを強制させる機能です。
例えばこんな感じにuse時に関数名を指定します。
use JSON qw(encode_json);
実は@EXPORTで宣言している場合もuse時に関数名を指定している場合はそのメソッドしかエクスポートされません。関数名を何も指定せずに()を書いた場合は何もエクスポートされないため、宣言的に関数名を書くようにするとほぼ@EXPORT_OKと同様の振る舞いをします。
自分が書いているコードに出てくるメソッドの宣言元をはっきりさせる単純な解決方法はEXPORTをやめ、Hoge::Foo->bar()の様にフルパスでメソッドを実行することですが、テストヘルパなどEXPORTした方が利便性が高いものもあるでしょう。
decode_utf8など、EXPORTされたメソッドを使うのがイディオム的に認知されているものもあります。
エクスポートした関数を使うのと、どこから取り込んできたのかの視認性を高めるには、use時にエクスポートしたい関数名を漏れなく書くしか無いでしょう。
しかしこれを人間がやるのは面倒!!! ということで作られたのがperlimportsです。
....と、ここまで書いたところで奇跡的に裏番組のはてなエンジニア Advent Calendar 2021で
id:papixさんが紹介している内容と前提知識が同じだった!!そんなことってあるんだ
あらためてApp::perlImports
- どこから来たのかわからないシンボルをエクスポートしている
useを特定したい @EXPORTを全部インポートするのではなく、一部だけ指定してインポートしたい
これらの課題に対して、golangのgoimportsを参考に、構文解析ライブラリPPIの力を借りて誕生したのがApp::perlimportsです。 metacpan.org
著者のOALDERSさんはLWPなどのメンテナもやられています。
使ってみる
例えば次の様なモジュールがあったとしましょう。
package MyPkg::One; use strict; use warnings; use Exporter 'import'; our @EXPORT = qw(f1); sub f1 { print "f1\n"; } 1;
package MyPkg::Two; use strict; use warnings; use Exporter 'import'; our @EXPORT_OK = qw(f2); sub f2 { print "f2\n"; } 1;
package MyPkg::Three; use strict; use warnings; use Exporter 'import'; our @EXPORT_OK = qw(f3); sub f3 { print "f3\n"; } 1;
package MyPkg::Four; use strict; use warnings; use Exporter 'import'; our @EXPORT = qw(f4); sub f4 { print "f4\n"; } 1;
これをhoge.plから以下の様に呼び出します。
use strict; use warnings; use MyPkg::One; use MyPkg::Two; use MyPkg::Three; use MyPkg::Four; f1(); f2(); MyPkg::Three->f3();
hoge.plの状況は次のとおりです。
f1はMyPkg::OneがEXPORTしているものを使っているf2はMyPkg::TwoがEXPORT_OKしているが、コード上は指定を忘れているf3はフルパスで呼び出しているf4はuseしているが特に使っていない
このコードはそのまま実行すると, f2の解釈で実行時エラーが発生します。また、どのモジュールからエクスポートされているのかパット見ではわかりません。
さらにMyPkg::Fourは必要がないf4をエクスポートしてしまっています。
❯ perl -Ilib hoge.pl f1 Undefined subroutine &main::f2 called at hoge.pl line 10.
これをApp::perlimportsに通すと、以下のようにコードが書き換わります。
❯ perlimports --libs lib hoge.pl use strict; use warnings; use MyPkg::One qw( f1 ); use MyPkg::Two qw( f2 ); use MyPkg::Three (); use MyPkg::Four (); f1(); f2(); MyPkg::Three->f3();
見ると次の様な変換が行われています。
f1,f2がEXPORT元のモジュールが明記されるようになった- エクスポートした関数を使用していない
ThreeとFourは、空配列を指定することになり、何もエクスポートしなくなっている
変換されたコードはそのまま動かす事ができます。(今回はパイプで動かしています) こうすると、実行時エラーを未然に防ぐことが可能になり、どのモジュールからエクスポートしているか、パット見でわかるようになりました。
❯ perlimports --libs lib hoge.pl | perl -Ilib
f1
f2
f3
さらに、無駄にロードしているMyPkg::Fourを削除することも可能です。
❯ perlimports --libs lib --no-preserve-unused hoge.pl
use strict;
use warnings;
use MyPkg::One qw( f1 );
use MyPkg::Two qw( f2 );
use MyPkg::Three ();
f1();
f2();
MyPkg::Three->f3();
こうすると、無駄なファイルをuseし続ける事もなくなり便利ですね!
インストール方法
cpanmもしくはcpmを使ってインストールしましょう
$ cpanm App::perlimports
$ cpm install -g App::perlimports
$ plenv rehash
使い方
CLIツールとして提供されているので、perltidyの様にコマンドで実行します。
$ perlimports --libs lib,local/lib/perl5 -f hoge.pl
主要なオプションを見てみます。
--libs-f- 変換対象のファイル名
--no-padding- perlimportsが変換したあと、モジュール名の前後に余計な空白をいれない
- デフォルトでは
use Foo qw( bar baz );の様になるが、--no-paddingを指定するとuse Foo qw(bar baz);になる
--inplace-edit,-i- デフォルトでは変換したコードは標準出力に出るが、このオプションを指定すると変換元のファイルを上書きする
--no-preserve-unused- 使っていなさそうなモジュールをuseしないようにする
注意点
自動でuseしてくれる訳ではない
下手にgoimportsを使ったことがあると誤解しやすい点で、perlimportsの(現状)の興味の対象はEXPORTしたシンボルをいい感じに特定することのみです。goimportsはimportの追加などもしていましたが、それに対応する機能はありません。モジュールのuseし忘れなどは興味の範囲外になっています。
例えばMyPkg::One->f1()の様なメソッド呼び出しをしていて、MyPkg::Oneをuseし忘れた場合、perlimportsはこれを補完しません。
また、そもそもuseしているモジュールがない状態で実行しても補完はしてくれません。
そのためモジュールのuse忘れなどのimportエラーは別系統で判断する必要があります。
❯ cat hoge.pl use strict; use warnings; use FindBin; use lib "$FindBin::Bin/lib"; use DDP {deparse => 1, hash_max => 0}; f1(); ~/workspace/test3 via 🐪 v5.34.0 ❯ perlimports --libs lib -f hoge.pl use strict; use warnings; use FindBin (); use lib "$FindBin::Bin/lib"; use DDP {deparse => 1, hash_max => 0}; f1();
関数エクスポート以外のuseは工夫の余地がある
また、Class::Accessor::LiteやClass::Enumemonのようなuse時にエクスポートする関数名を指定する以外に、独自にimportsを定義しているモジュールは、perlimportsはuseしたものを使っていないと見なしてしまいます。これはperiimportsの作者によって名指しでperlimportsの自動整形下に置かない様に書かれているモジュール群か、構文解析の結果自動調整を逃れたもの以外は、空配列のimportに変換されてしまいます。
❯ cat hoge.pl use strict; use warnings; use FindBin; use lib "$FindBin::Bin/lib"; use DDP {deparse => 1, hash_max => 0}; use Class::Accessor::Lite ( rw => [qw/ hoge/], ); f1(); ~/workspace/test3 via 🐪 v5.34.0 ❯ perlimports --libs lib,local/lib/perl5 -f hoge.pl use strict; use warnings; use FindBin (); use lib "$FindBin::Bin/lib"; use DDP {deparse => 1, hash_max => 0}; use Class::Accessor::Lite (); f1();
これを回避するには、調整したくないモジュールをuseしている行の後ろに特定のアノテーション(## no perlimports)を書くか*3、コマンド実行時の引数でモジュール名を直接列挙、またはモジュール名が列挙されたファイルを渡すことで回避可能です。この指定には正規表現が使用可能です。
作者にPRを送るとignoreするモジュールに追加してくれる様なので、普段使うモジュールで管理されるとまずいものがあればPRを送ると良いでしょう。
~/workspace/test3 via 🐪 v5.34.0 ❯ cat hoge.pl use strict; use warnings; use FindBin; use lib "$FindBin::Bin/lib"; use DDP {deparse => 1, hash_max => 0}; use Class::Accessor::Lite ( rw => [qw/ hoge/], ); f1(); ~/workspace/test3 via 🐪 v5.34.0 ❯ perlimports --libs lib,local/lib/perl5 --ignore-modules-pattern '^Class::' -f hoge.pl use strict; use warnings; use FindBin (); use lib "$FindBin::Bin/lib"; use DDP {deparse => 1, hash_max => 0}; use Class::Accessor::Lite ( rw => [qw/ hoge/], ); f1();
メソッドの追加などの副作用で持つモジュールをuseしている場合は、--no-preserve-unusedに気をつける
Perlのモジュールでは、useすると自分以外のモジュールのメソッドをオーバーライドやメソッドを名前空間に追加するものがあります。
例えばHTTP::Message::PSGIは、useするとHTTP::Request名前空間にto_psgiがメソッドとして追加されます。
これはuseすることで効果を発揮するモジュールであるため、HTTP::Message::PSGIがEXPORTしているメソッドを直接使っていないケースや、HTTP::Message::PSG->で何かを呼び出しているコードが無い限り、perlimportsで--no-preserve-unusedオプションを付けて実行してしまうと、何も意味がないモジュールを呼び出しているとuseを削除されてしまいます。
実際はHTTP::Message::PSGIはperlimports側で明示的に整形する対象に含まないように指定されているので、この様なことは起きないのですが、自分たちで書いているライブラリに似た挙動があるものがあれば、使用する際は気をつけましょう。
明日は
id:mp0liiu さんで、immutableなコレクションを作るです!
*1:動的ロードなどを考慮するとこの限りではない
*3:ブロック的に囲うことも可能です
https://metacpan.org/pod/perlimports#ANNOTATIONS/IGNORING-MODULES
PerlのXSをlldbでデバッグする
興味が湧いたのでPerlのC拡張であるXS言語で書かれているList::Utilを読んでいるのだけど、C言語チックなものを読むときはやはりデバッガが使いたい。 XSも結局C言語なのでgdb/lldbでデバッグできないかなと思ってググったところ、できそうなエントリを見つけたので試していく。
XSのデバッグビルド
C言語をデバッグにはCで書かれたコードにデバッグオプションを付けてビルドする必要がある。 XSも同様だと思われるので、何かしらの方法でデバッグビルドする必要がある。
XSモジュールに限らずPerlモジュールのビルドは、Makefile.PLかBuild.PLのどちらかのビルドツールがよく使われている。 Makefile.PLは名前の通りMakefileを生成してくれる君で、Build.PLはModule::Buildを前提としたシステムになっている。 詳細はモダンPerlの世界へようこそ 第23回 Module::Build:MakeMakerの後継者を目指してが詳しい。
今回は読みたいコードがList::Utilで、List::UtilはMakefile.PLを使っていたのでこちらの雰囲気を見る。
最終的にMakefileを生成しにいくのはExtUtils::MakeMakerが提供しているWriteMakefileを実行するタイミングである。
List::UtilのMakefile.PLの場合は一番最後の行でこのように実行している。
WriteMakefile(%params);
この引数%paramsにはビルドに関する様々な情報を入れる事ができる。
ExUtils::MakeMakerのドキュメントを見ると、XSの最適化の度合いを調整出来るOPTIMIZEがオプションとして存在している。
ドキュメントを見る限りCの最適化オプションをそのまま渡せるので、デバッグしたかったら-O0 -gあたりを渡せばよいだろう。
ということでList::Utilのparamsを作っている箇所の一番下にしれっとOPTIMIZEを指定する。
my %params = ( NAME => q[List::Util], ABSTRACT => q[Common Scalar and List utility subroutines], AUTHOR => q[Graham Barr <gbarr@cpan.org>], DEFINE => $defines, DISTNAME => q[Scalar-List-Utils], VERSION_FROM => 'lib/List/Util.pm', OPTIMIZE => '-g -O0',
準備は整ったのでビルドしていく。まずはMakefileを生成する必要があるので、Makefile.PLを実行する。
❯ perl Makefile.PL Checking if your kit is complete... Looks good Generating a Unix-style Makefile Writing Makefile for List::Util Writing MYMETA.yml and MYMETA.json
無事Makefileができたので素朴にmakeする
❯ make
cp lib/Scalar/Util.pm blib/lib/Scalar/Util.pm
cp lib/List/Util.pm blib/lib/List/Util.pm
cp lib/Sub/Util.pm blib/lib/Sub/Util.pm
cp neko.pl blib/lib/List/neko.pl
cp lib/List/Util/XS.pm blib/lib/List/Util/XS.pm
Running Mkbootstrap for Util ()
chmod 644 "Util.bs"
"/Users/anatofuz/.plenv/versions/5.32.0/bin/perl5.32.0" -MExtUtils::Command::MM -e 'cp_nonempty' -- Util.bs blib/arch/auto/List/Util/Util.bs 644
"/Users/anatofuz/.plenv/versions/5.32.0/bin/perl5.32.0" "/Users/anatofuz/.plenv/versions/5.32.0/lib/perl5/5.32.0/ExtUtils/xsubpp" -typemap '/Users/anatofuz/.plenv/versions/5.32.0/lib/perl5/5.32.0/ExtUtils/typemap' ListUtil.xs > ListUtil.xsc
mv ListUtil.xsc ListUtil.c
cc -c -fno-common -DPERL_DARWIN -mmacosx-version-min=11.2 -fno-strict-aliasing -pipe -fstack-protector-strong -DPERL_USE_SAFE_PUTENV -g -O0 -DVERSION=\"1.56\" -DXS_VERSION=\"1.56\" "-I/Users/anatofuz/.plenv/versions/5.32.0/lib/perl5/5.32.0/darwin-2level/CORE" -DPERL_EXT -DUSE_PPPORT_H ListUtil.c
rm -f blib/arch/auto/List/Util/Util.bundle
cc -mmacosx-version-min=11.2 -bundle -undefined dynamic_lookup -fstack-protector-strong ListUtil.o -o blib/arch/auto/List/Util/Util.bundle \
\
chmod 755 blib/arch/auto/List/Util/Util.bundle
Manifying 4 pod documents
無事makeが実行できた。
ちなみにmake cleanとするとMakefileごとビルドしたものを消してくれるので便利。
デバッグビルドしたXSをPerlから呼び出す
ビルドするともとのxsのコードを純粋なC言語*1に変換されたものなどが生成される。 実際にPerlインタプリタが読みこめるファイル形式のものはblib以下に書き出される。
blibディレクトリはMakeMakerがビルドしたファイルが含まれている。 List::Utilの場合は次のようなファイルが含まれている。
❯ tree blib blib ├── arch │ └── auto │ └── List │ └── Util │ └── Util.bundle ├── bin ├── lib │ ├── List │ │ ├── Util │ │ │ └── XS.pm │ │ └── Util.pm │ ├── Scalar │ │ └── Util.pm │ ├── Sub │ │ └── Util.pm │ └── auto │ └── List │ └── Util ├── man1 ├── man3 │ ├── List::Util.3 │ ├── List::Util::XS.3 │ ├── Scalar::Util.3 │ └── Sub::Util.3 └── script 16 directories, 9 files
ちなみにUtil.bundleの場合はバイナリファイルだったりする。
普通のPerlのライブラリの場合はlib以下のものを@INCに入れれば実行できた。
似た感じでblib以下のものをPerlに教えてあげれば自分でビルドしたXSをPerlインタプリタから呼び出す事ができる。
blibをPerlスクリプトから呼び出すにはblibモジュールを使う方法もあるけれど、勝手にカレントのblibディレクトリ以下を読み込んでくれるExtUtils::testlibを使うのが便利。
というわけで自分でビルドしたList::Utilを呼び出すサンプルコードは次のようになる。今回はめんどくさいのでビルドしたディレクトリ上でやっている。
use strict; use warnings; use ExtUtils::testlib; use List::Util qw/all/; my $hoge = [1,2,3]; if (all { $_ } @$hoge) { print "hello!\n"; }
xsのデバッグ実行
さて例題も書けたのでデバッグしていこう。 一点ポイントとなるのは、このblibを実行できるのは、XSをビルドする際に利用したPerlじゃなくと実行ができない。
今回では上のログにある通り/Users/anatofuz/.plenv/versions/5.32.0/bin/perl5.32.0が実行したPerlのバイナリである。
これ以外のPerlのバイナリ、例えば/usr/bin/perlで実行すると以下のような感じでモジュールロードに失敗する。
❯ /usr/bin/perl neko.pl Can't load '/Users/anatofuz/src/github.com/Dual-Life/Scalar-List-Utils/blib/arch/auto/List/Util/Util.bundle' for module List::Util: dlopen(/Users/anatofuz/src/github.com/Dual-Life/Scalar-List-Utils/blib/arch/auto/List/Util/Util.bundle, 0x0001): symbol '_PL_DBsub' not found, expected in flat namespace by '/Users/anatofuz/src/github.com/Dual-Life/Scalar-List-Utils/blib/arch/auto/List/Util/Util.bundle' at /System/Library/Perl/5.30/darwin-thread-multi-2level/DynaLoader.pm line 197. at neko.pl line 4. Compilation failed in require at neko.pl line 4. BEGIN failed--compilation aborted at neko.pl line 4.
ビルド時に使ったPerlだと問題なく実行できる。
❯ /Users/anatofuz/.plenv/versions/5.32.0/bin/perl5.32.0 neko.pl hello!
ではデバッガをかけて実行してみる。今回はlldbでやる。
❯ lldb -- /Users/anatofuz/.plenv/versions/5.32.0/bin/perl neko.pl (lldb) target create "/Users/anatofuz/.plenv/versions/5.32.0/bin/perl" Current executable set to '/Users/anatofuz/.plenv/versions/5.32.0/bin/perl' (arm64). (lldb) settings set -- target.run-args "neko.pl" (lldb)
今回デバッグしたいxsのファイルはListUtil.xsで、使っているallのコードを見てみたい。
allは実はanyのエイリアスで、any関数は702行目から本体がある。
というわけでListUtil.xsの702行目にbreak pointを貼ってみる。
(lldb) b ListUtil.xs:702 Breakpoint 1: no locations (pending). WARNING: Unable to resolve breakpoint to any actual locations.
そして実行すると見事に止まる。
(lldb) process launch
Process 69006 launched: '/Users/anatofuz/.plenv/versions/5.32.0/bin/perl' (arm64)
1 location added to breakpoint 1
Process 69006 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
frame #0: 0x00000001004ae5a0 Util.bundle`XS_List__Util_any(cv=0x00000001010fd810) at ListUtil.xs:702:22
699 PROTOTYPE: &@
700 PPCODE:
701 {
-> 702 int ret_true = !(ix & 2); /* return true at end of loop for none/all; false for any/notall */
703 int invert = (ix & 1); /* invert block test for all/notall */
704 GV *gv;
705 HV *stash;
Target 0: (perl) stopped.
これで止まるとこっちのものなので、あとはnextしたり値をprintしたりして確かめていくと普通に読んでいける。 読み方としてはPerlインタプリタ読んでるのと同じなので、SVの中身に思いを馳せる感じになっていくと思う。
というわけでXSも実はlldbで読める!!! 読んでいくぞ!!!