Shogo's Blog

Dec 10, 2021 - 3 minute read - perl

Perl 5.35.4 の defer を先取り

この記事は、Perl Advent Calendar 2021 の10日目の記事です。 9日目は @shogo82148 で「Perl 5.34.0 の try-catch を触ってみる」でした。


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

defer 構文は安定版にはまだ取り込まれていません。 特に断りのない限り 2021-12-10 現在の最新開発版 Perl 5.35.6 で動作確認をしています。

まずは Perl 5.35.6 をビルドする

Perl 5.35.6 は開発版なのでビルド済みのバイナリは配布されていません。 しかし plenv を使っていれば特に難しいことはありません。 注意点は開発版の警告を抑制するために -Dusedevel オプションをしていることくらいです。

plenv install 5.35.6 -Dusedevel
plenv local 5.35.6

defer を使ってみる

使い方はいつものように use feature プラグマで有効化し、 defer BLOCK とするだけ。

use strict;
use warnings;
use feature 'say';
use feature 'defer';
 
{
    say "This happens first";
    defer { say "This happens last"; }
 
    say "And this happens inbetween";
}

1;

出力:

defer is experimental at defer.pl line 8.
This happens first
And this happens inbetween
This happens last

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

use strict;
use warnings;
use feature 'say';
use feature 'defer';
no warnings 'experimental::defer'; # 警告を抑制

{
    say "This happens first";
    defer { say "This happens last"; }
 
    say "And this happens inbetween";
}

1;

出力:

This happens first
And this happens inbetween
This happens last

これまでの defer

Scope::Guard モジュール

Perl のガーベージコレクションがリファレンスカウントだということを利用して defer を実現するモジュールです。

use strict;
use warnings;
use feature 'say';
use Scope::Guard qw(guard);

{
    say "This happens first";
    my $guard = guard { say "This happens last"; };
 
    say "And this happens inbetween";
}

1;

出力:

This happens first
And this happens inbetween
This happens last

変数の寿命でスコープを抜けたことを検知するので、必ず変数に格納しなければならないのがちょっとした罠です。 後の処理で一切使ってないんですけどね。

また guard { ... } は try-catch のときと同じようにブロックに見せかけた無名関数なので、 caller 関数の値が変わるという罠に注意です。

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

キーワードプラグインのSyntax::Keyword::Defer モジュール です。

use strict;
use warnings;
use feature 'say';
use Syntax::Keyword::Defer;
 
{
    say "This happens first";
    defer { say "This happens last"; }
 
    say "And this happens inbetween";
}

1;

Perl 5.35.4 から利用可能になった defer 構文と使い方はほぼ一緒です。 try-catch と同様に、古いPerlではSyntax::Keyword::Defer モジュールにフォールバックする Feature::Compat::Defer があります。

Go との違い

僕の知る限り、組み込みで defer に対応しているのは Go 言語があります。 Perl と Go では defer の実行タイミングが違うので要注意です。 (どちらかというと Go のほうが間違えやすい気がする)

Perl の defer はスコープを抜けるときに実行されます。

use strict;
use warnings;
use feature 'say';
use feature 'defer';
no warnings 'experimental::defer';

sub do_something {
    {
        defer { say "Hello, 世界"; }
    
        say "スコープを抜けた直後に実行される";
    }
    say "関数の最後ではない";
}

do_something();

1;

実行結果:

スコープを抜けた直後に実行される
Hello, 世界
関数の最後ではない

一方 Go は関数を抜けるときに実行されます。

package main

import "fmt"

func do_something() {
	{
		defer fmt.Println("Hello, 世界")
		fmt.Println("スコープを抜けた直後ではなく")
	}
	fmt.Println("関数を抜けた直後に実行される")
}

func main() {
	do_something()
}

実行結果:

スコープを抜けた直後ではなく
関数を抜けた直後に実行される
Hello, 世界

また変数の扱いも違います。 Perl では defer ブロック内で使っている変数を、 defer の後で書き換えるとその変更は defer の実行時に反映されます。

use strict;
use warnings;
use feature 'say';
use feature 'defer';
no warnings 'experimental::defer';

sub do_something {
    my $hello = "世界";
    defer { say "Hello, $hello"; }
    $hello = "chooblarin";
}

do_something();

1;

実行結果:

Hello, chooblarin

一方 Go では defer で使っている変数を後から書き換えても、 defer 実行時には反映されません。 これも Go の挙動が罠っぽい。

package main

import "fmt"

func do_something() {
	hello := "世界"
	defer fmt.Println("Hello,", hello)
	hello = "chooblarin"
}

func main() {
	do_something()
}

実行結果:

Hello, 世界

まとめ

  • 次のリリース Perl 5.36.0 から defer が使えるよ
  • 開発版で良ければ Perl 5.35.4 から使えるよ

明日10日は @shogo82148 で「Perl 5.35.5 の iterating over multiple values at a time を先取り」です。お楽しみに!

参考