Shogo's Blog

Dec 9, 2021 - 4 minute read - perl

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

この記事は、Perl Advent Calendar 2021 の9日目の記事です。 8日目は @doikoji で「Getopt::Longのスペルが覚えられない俺はとうとう覚える努力を放棄してラッパーを作った」でした。


アドベントカレンダー25日もあると疲れてくるので、今日はかる~く行きましょう。 Perl 5.34.0 から利用可能になった try-catch 構文を触ってみたというお話です。

特に断りのない限り 2021-12-09 現在の最新安定版 Perl 5.34.0 で動作確認をしています。

とりあえず使ってみる

使い方は簡単です。 use feature プラグマで有効化し、 try BLOCK catch (VAR) BLOCK とするだけ。 最初の try ブロックの中で die すると catch ブロックが実行されます。

use strict;
use warnings;
use feature qw(try);

try {
    die "dead";
} catch($e) {
    print "catch: $e";
} # no more ";" here !!!

1;

出力:

try/catch is experimental at try-catch.pl line 5.
try/catch is experimental at try-catch.pl line 7.
catch: dead at try-catch.pl line 6.

Perl 5.34.0 ではまだ実験的な機能扱いなので警告がでます。 no warnings プラグマで抑制が可能です。

use strict;
use warnings;
use feature qw(try);
no warnings "experimental::try"; # 警告の抑制

try {
    die "dead";
} catch($e) {
    print "catch: $e";
} # no more ";" here !!!

1;

出力:

catch: dead at try-catch.pl line 6.

これまでの例外処理

これまでは例外という概念がなかったので、Perl Monger たちはいろんな方法で「例外っぽいもの」を作り出してきました。

eval

言語の組み込み機能で例外を作り出すには dieeval を使います。

use strict;
use warnings;

eval {
    die "dead";
}; # ";" is required !!!
if ($@) {
    print "catch: $@";
}

1;

";" is required !!! と書いたところのセミコロン忘れるというのがよくあるミスです。 また $@ という特殊変数の扱いが少し厄介です。 覚えにくいというのもそうなんですが、グローバル変数なので意図せず書き換わってしまう場合があるそうです。 正直自分も詳しくないので詳細は Try::Tiny#BACKGROUND をどうぞ。

Try::Tiny モジュール

3rd-party のモジュールを使う方法です。 Try::Tiny はたぶん僕が一番お世話になったモジュールです。

use strict;
use warnings;
use Try::Tiny;

try {
    die "dead";
} catch {
    print "catch: $_";
}; # ";" is required !!!

1;

だいぶ try-catch 構文に近くなりますが、";" is required !!! と書いたところにやっぱりセミコロンが必要です。

また { ... } はブロックではなく無名関数の省略記法だというのもよくある罠です。 省略せずに書くとこうなります。

use strict;
use warnings;
use Try::Tiny;

try(
    sub {
        die "dead";
    },
    catch(
        sub {
            print "catch: $_";
        },
    ),
);

1;

この違いがどんな罠を生むかというと、ブロックと関数では制御構文の挙動がことなるという点です。 例えば return の場合、do_something 関数を抜けるつもりで return を書いても抜けることは出来ません。

use strict;
use warnings;
use Try::Tiny;

sub do_something {
    try {
        return; # do_something を抜けたい
    } catch {
        print "catch: $_";
    };
    print "この行は実行されてしまう\n";
}

do_something();

1;

一方 5.34.0 で導入された try はブロックなので関数を抜けることができます。

use strict;
use warnings;
use feature qw(try);
no warnings "experimental::try";

sub do_something {
    try {
        return; # do_something を抜けたい
    } catch ($e) {
        print "catch: $e";
    }
    print "この行は実行されない!!\n";
}

do_something();

1;

また caller 関数の戻り値が変化するという罠も潜んでいます。

use strict;
use warnings;
use Try::Tiny;

sub do_something {
    try {
        my ($package, $filename, $line) = caller;
        print "$package, $filename, $line\n";
    } catch {
        print "catch: $_";
    };
}

do_something();

1;

実行結果:

Try::Tiny, /Users/shogo.ichinose/.plenv/versions/5.34.0/lib/perl5/site_perl/5.34.0/Try/Tiny.pm, 102

組み込みの die はスタックトーレスを埋め込んでくれないので、 caller 関数を使ったヘルパーを時々書くのですが、 この挙動を把握しておかないと痛い目にあいます。 エラーが起きたからログを調査しよう!といったときに Try/Tiny.pm line 102 という文字列だけ残されていて何度絶望したことか・・・

一方 5.34.0 で導入された try はブロックなので caller は変化しません。

use strict;
use warnings;
use feature qw(try);
no warnings "experimental::try";

sub do_something {
    try {
        my ($package, $filename, $line) = caller;
        print "$package, $filename, $line\n";
    } catch ($e) {
        print "catch: $e";
    }
}

do_something();

1;

実行結果:

main, try-caller.pl, 15

Syntax::Keyword::Try モジュール

Syntax::Keyword::Try はキーワードプラグインという仕組みで Perl の構文自体を書き換えてしまうモジュールです。

use strict;
use warnings;
use Syntax::Keyword::Try;

try {
    die "dead";
} catch($e) {
    print "catch: $e";
} # no more ";" here !!!

1;

{ ... } はれっきとしたブロックなので returndo_something 関数を抜けることができます。

use strict;
use warnings;
use Syntax::Keyword::Try;

sub do_something {
    try {
        return; # do_something を抜けたい
    } catch ($e) {
        print "catch: $e";
    }
    print "この行は実行されない!!\n";
}

do_something();

1;

5.34.0 で導入された try-catch とほぼ互換性があるのですが、 caller 関数の挙動だけ少し違います。

use strict;
use warnings;
use Syntax::Keyword::Try;

sub do_something {
    try {
        my ($package, $filename, $line) = caller;
        print "$package, $filename, $line\n";
    } catch ($e) {
        print "catch: $e";
    }
}

do_something();

1;
main, syntax-keyword-try-caller.pl, 9

まあ結果を見る限り Try/Tiny.pm line 102 よりは100倍マシ・・・。

今後について

Feature::Compat::Try モジュール

Perl 組み込みの try-catch を使いたいけど古い Perl を切りたくない、という欲張りな人は Feature::Compat::Try モジュールが使えます。 Perl のバージョンに応じて feature プラグマと Syntax::Keyword::Try モジュールを切り替えてくれます。

use strict;
use warnings;
use Feature::Compat::Try;

try {
    die "dead";
} catch($e) {
    print "catch: $e";
} # no more ";" here !!!

1;

ただし先に書いたように 5.34.0 で導入された try-catch と Syntax::Keyword::Try モジュールでは caller 関数の挙動が違う点に注意が必要です。

finally ブロック

try-catch の導入によって近代化への一歩を踏み出した (?) かのように思える Perl ですが、 なんと多くの言語に存在する finally ブロックには対応していません。

まあ、まだ「実験的な機能」ですからね。 先に紹介した Feature::Compat::Try でも、 Syntax::Keyword::Try へフォールバックする際はご丁寧に finally を無効化しています。

“Pre-RFC” として議論されているのは見つけたけど、どういう形で実装されるのかは未定のようです。

まとめ

  • Perl 5.34.0 から use feature qw(try); で try-catch 構文が使えます
  • Feature::Compat::Try モジュールを使うと古い Perl でも使えるよ
  • 罠が少ないので便利

明日10日は @shogo82148 で「Perl 5.35.4 の defer を先取り」です。お楽しみに!

参考