Perlでtry-catch-finally文を使いたい(2022年版?)

以下の過去記事への自己レスのようなものです。

ryozi.hatenadiary.jp

コメントで教えていただいたり、try featureが追加されたりしていたのでこれを機に色々使ってみました。

あと、タイトルから「(Javaのような)try-catch-finally文をPerlで行う方法」と取れるような気がしており、おそらく記事の内容のような欠陥だらけの「try-catch-finally文っぽいものの作り方」を知りたい人は少数でしょうから、実際に行いたい人向けにどうするかをちょっと調べました。

要約

5.34にtry featureが追加されておりこれを使うべきです

古いPerlのバージョンであればXSを使うものの Syntax::Keyword::Try がちゃんと動いている気がします。

その他は(ちゃんと動くモノと比べると)うまく動かないケースもあるようだったので、注意して使いましょう。

調査

調査に使ったコードはGitHubに貼っておきました。

https://github.com/ryozi-tn/p5-try-catch-finally-2022

雑に調べた感じ以下の感じでした。

Try::Tiny Nice::Try Try::Harder Syntax::Keyword::Try Perl 5.34のtry feature
Perl バージョン 5.10 5.16 5.10 5.14 5.34
Cコンパイラ - 必要(依存パッケージ) - 必要 -
仕組み プロトタイプ宣言 Source Filter (Filter::Util::Call) Source Filter (Filter::Simple) XS? 組み込み?
try中のcaller × ◯変だが取れる ×finallyの末端
try中のreturn × ◯(1.3.3で確認)
catch中のreturn × ◯(1.3.3で確認)
catch中のdie ◯(1.3.3で確認) ?
last ラベル ◯ 警告が出るが動く ×
redo ラベル ◯ 警告が出るが動く × × finally後の処理が呼ばれない?

(catchの変数指定とか、finallyでreturnとか、finally無しとか足りない検証もありますが面倒なので...)

Try::Tiny

https://metacpan.org/pod/Try::Tiny

前の記事でコメントで「お前がやったことは再発明だよ」と紹介いただきました。

古き良きライブラリのようです。Perl v5.10でも動作します。

ただし、try中のreturnの挙動はtryのブロックを抜けるだけなので注意点があります。

Nice::Try

https://metacpan.org/pod/Nice::Try

前の記事でコメントで紹介いただきました。

Tiny::Tryとは異なり、文末のセミコロンが不要でより自然に書けます。

また、例外の変数名を指定できたり、例外クラスを定義してキャッチできるといった不思議なコード(後述)が特徴です。

Perl v5.16でも動作しますが、依存パッケージの都合でCコンパイラが必要になります。Pure Perl厨はご注意ください。

ただし、私がv1.3.1で試した感じではtry中のreturnの結果をを使わないコードだと異なる動きをします(バグっぽい?)

# サブルーチンの戻り値を代入(戻り値を使う)
$_ = (sub{ 
    try { return "OK" }catch{print "catch: Unreachable code\n";} finally {print "finally\n";} 
    print "Unreachable code?\n"; # 呼ばれない
})->();

# サブルーチンの戻り値を捨てる
(sub{ 
    try { return "OK" }catch{print "catch: Unreachable code\n";} finally {print "finally\n";}
    print "Unreachable code?\n"; # 呼ばれた
})->();

2023-01-13 追記: 作者様からコメントで修正された旨を受け再度確認し、1.3.3でtryとcatch中のreturnでvoid contextのとき(=戻り値を使わないとき)の挙動、catch中のdieでfinallyが呼ばれなかった点が修正されたことを確認しました

Try::Harder

https://metacpan.org/pod/Try::Harder

Nice::TryのSEE ALSOに書かれていて使ってみました。

Syntax::Keyword::Try をPure Perlで模擬したモノらしく、Syntax::Keyword::Tryがあればそちらを使うようで、無い場合はSource Filter(Filter::Simple)を使って頑張るようです。

redoラベルで期待していない挙動をしましたが、概ね良い動作をしているように見えます。

ただ、おそらくredo/lastなどのラベルを使ったコードを書いてると変な挙動をし始めます。コードをコピペして増やすとコンパイルエラーが起き始めました。(コピペしたコードをコメントアウトしても正規表現で置き換えるせいか影響を受けたり)

Syntax::Keyword::Try

https://metacpan.org/pod/Syntax::Keyword::Try

Nice::TryのSEE ALSOに書かれていたり、Try::Harderでも言及されていたので使ってみました。

XSを使うものの、Perl 5.34のtry featureと同じぐらい動きました。

Perl 5.34のtry feature

https://metacpan.org/release/XSAWYERX/perl-5.34.0/view/pod/perlsyn.pod#Try-Catch-Exception-Handling

こちらに書かれていることを鵜呑みにしました。

Perl 5.34.0 の try-catch を触ってみる

一番良い選択になるでしょう。

メモ

perl のdockerのベースイメージ

Official Imageが色々用意されています。古めなバージョンはタグ名が微妙に異なるので注意でv5.16は、FROM perl:5.16-slim-stretchとすればよいです。

*-slimは名前の通りサイズがスリムなイメージ。bashはあるので検証作業は行えるが、lessとかviとか当たり前のようにあるコマンドがないので注意です(編集系のコマンドはコンテナアプリでは不要なのでこんなもんでしょう)

Feature::Compat::Tryは?

https://metacpan.org/pod/Feature::Compat::Try

Syntax::Keyword::Tryと Perl 5.34のtry featureを切り替えて使うだけのようでした。 実際に試した感じ、5.34以前の古いバージョンではCコンパイラが必要ですが、5.34は不要でインストールできます。

Nice::TryやTry::Harderの catch($e) や セミコロン省略はどう実現しているのか?

PerlのSource filterを利用してます。Perlはコードをそのまま実行するのではなく、Source filterを通したコードを実行しているそうです。

Source filterは文字列しか扱えないので、Try::Harderは正規表現でマッチングしていたり、 Nice::TryではPerlの文脈を理解するためにPPIでパースしながら解釈しているようです。

Nice::Tryのソースコードを眺めて序盤で心が折れたのでメモしておきます。(頑張って構文を読んで処理を作り変えてはいそうだが…)

https://gitlab.com/jackdeguest/Nice-Try/-/blob/a84f0573/lib/Nice/Try.pm

  • L65: sub importの処理でソースフィルタを追加する。Filter::Util::Callのfilter_addを利用
  • L94-123: Filter::Util::Callの仕様よりfilterメソッドが呼び出される?ソースコード$codeへ読み込む(多分1行づつwhileで回して$codeへ追記している?)
  • L132-136): 読み取ったソースコードPPIでパースし、_parseメソッドへ渡す
  • L294-298: コードからtryで始まるStatement(文)を探してその参照を配列へ記録しておく
  • L308: try ~のコードの数だけ繰り返す
    • L321-330: tryかつコードブロックがあればtmp_refに詰め込みtmp_nodesをリセット。そうでない場合はtmp_nodesに詰め込む

よくわからなくなったのでここまで