Mobile Factory Tech Blog

技術好きな方へ!モバイルファクトリーのエンジニアたちが楽しい技術話をお届けします!

Perl5.36の変更点

こんにちは、エンジニアの id:mp0liiu です。

少し前の話になりますが、5/28にPerlの最新安定バージョンである5.36がリリースされたので、コミュニティ周りの動向も含めて気になった点についてまとめていこうと思います。

use v5.36

一番影響がある変更は use VERSION の効果が変わったことです。
use v5.34 以前はバージョンチェック、要求されたバージョンで利用可能なすべての機能(featureバンドル)の有効化、strict の有効化を行っていましたが、 use v5.36 からは warnings も有効化されるようになりました。

use v5.36;

my $str;
say $str; # Use of uninitialized value $str in say at ...

1行だけで strict, warnings, 最新の機能の有効化ができて便利なのと、perl開発チームも use VERSION をもっと普及させたいと考えているよう*1なので積極的に使っていきましょう!

use v5.36 で無効化される機能

5.36 の feature バンドルでは、Perlの構文解析を難しくしたりわかりにくい挙動をしてしまう原因になりがちだった古い機能の一部が無効化されるようになりました。

間接オブジェクト記法

間接オブジェクト記法というのは通常メソッドの呼び出しは次のように書くところを

my $ua = LWP::UserAgent->new(timeout => 10);
my $response = $ua->get('http://example.com');

このように書く記法のことです。

my $ua = new LWP::UserAgent timeout => 10;
my $response = get $ua 'http://example.com';

間接オブジェクト記法が無効になって特に嬉しいのは Try::Tiny をuseし忘れたときの謎の挙動が発生しないことだと思います。

例えば 5.34 だとこのコードは間接オブジェクト記法で解釈できてしまい、 try {} の中だけ実行して catch {} の部分は実行しないという挙動になってしまいますが、5.36 だとsyntaxエラーになります。

use v5.34;

try {
    die 'hoge';
} catch {
    if ($_ eq 'hoge') {
        say 'ok';
    }
    else {
        die $_;
    }
};

擬似的な多次元配列

まだPerlで多次元配列が作れなかった頃にハッシュに複数のキーを与えることで擬似的な多次元配列が作れるようになっていたのですが、その機能が無効になります。

use v5.36;

my %hash;
$hash{1, 2}; # Multidimensional hash lookup is disabled

安定化した実験的機能

以下の実験的機能が安定化して use v5.36use feature 機能名 で使えるようになりました。
これらの機能はプロダクトの中でも積極的に利用しても問題ないでしょう。

サブルーチンシグネチャ

perl5.18 で追加されたサブルーチンシグネチャがperl5.36でようやく安定化して @_ の中身を取り出さなくても引数を受け取ることができるようになりました。

# サブルーチンシグネチャを使わない場合
sub add {
  my ($x, $y) = @_;
  return $x + $y;
}

# サブルーチンシグネチャを使う場合
use feature 'signatures';

sub add2 ($x, $y) {
  return $x + $y;
}

use feature 'signatures'; した状態でもサブルーチンシグネチャを使ったサブルーチンと従来と同じ引数を @_ から取り出すサブルーチンは共存できますが、サブルーチンプロトタイプを使っているサブルーチンは :prototype 属性を使わないと共存できません。

use feature 'signatures';

sub mymap (&@) { # -> syntax error!
  my ($code, @args) = @_;
  my @returns;
  for my $arg (@args) {
    local $_ = $arg;
    push @returns, $code->();
  }
  return @returns;
}

sub mymap :prototype(&@) ($code, @args) {
  my @returns;
  for my $arg (@args) {
    local $_ = $arg;
    push @returns, $code->();
  }
  return @returns;
}

また、次のようにサブルーチンシグネチャを使ったサブルーチンの中で @_ を参照することは実験的機能とされているので注意してください。

sub add ($x, $y) {
  my ($x2, $y2) = @_; # Use of @_ in list assignment with signatured subroutine is experimental
  return $x2 + $y2;
}

サブルーチンシグネチャには型は指定できないので Data::Validator, Smart::Args などの引数バリデータはまだ手放せないですが、書捨てのスクリプトを書くときや初学者のとっつきやすさはかなり良くなったのではないでしょうか。

isa 演算子

isa 演算子は左被演算子に渡した値が右被演算子のクラスのインスタンスまたはそこから派生したクラスのインスタンスなのかを調べる演算子でperl5.32 で実験的機能として追加されましたが、今回の変更で警告はなくなりました。
今まで Scalar::Util::blessedisa メソッドでクラスのインスタンスかどうかを判定していたところなどを簡潔に書けるようになると思います。

use Test::More;
use feature 'isa';
  
package Hoge {
  sub new { bless +{}, shift }
}
my $obj = Hoge->new;

# isa 演算子を使わない場合
use Scalar::Util qw( blessed );
ok blessed($obj) && $obj->isa('Hoge');

# isa 演算子を使う場合
ok $obj isa Hoge;

done_testing;

追加された実験的機能や改善された点

真偽値が安定して追跡できるようになった

今まで!!0!!1 といった構文や真偽値を返す式、コア関数から返されていた真偽値はなんらかの変数に代入すると真偽値としての性質を失ってしまっていたそうですが、 5.36からは変数に代入してもその真偽値としての性質を保持するようになったそうです。

後述する builtin の新しい関数 is_bool() で値が真偽値としての性質を持つかどうかをチェックすることができます。

この変更によって、他の言語との相互運用やデータ型のシリアライゼーションが簡単になりました。
例えば JSON::PP でエンコードする場合、今までは真偽値だと明示するには true の場合 \1 を、false の場合 \0 を渡す必要があり知識がないとハマりがちだったのですが、 5.36 以降は !!1, !!0 など真偽値として返された値(後述の builtin::true, builtin::false の値も含む)をそのまま渡せばちゃんとエンコードできるようになりました。

use v5.34;
use JSON::PP qw( encode_json );

my $data = +{ hoge => \0 };
say encode_json($data); # {"hoge":false}
use v5.36;
use JSON::PP 4.09 qw( encode_json );  # 真偽値でそのままエンコードするには JSON::PP 4.09 以上が必要

my $data = +{
    hoge => !!0 # builtin::false でも可
};
say encode_json($data); # {"hoge":false}

変数に代入しても真偽値としての性質を保持するとはどういうことか?

「変数に代入しても真偽値としての性質を保つ」というのがどういうことかわからなかったので、5.34、5.36それぞれで真偽値を変数に代入した場合の性質の変化を調べてみました。

use v5.34;

use Devel::Peek qw( Dump );

Dump !!1;

my $bool = !!1;
Dump $bool;

my $one = 1; $one . '';
Dump $one;
5.34
SV = PVNV(0x236c030) at 0x937720
  REFCNT = 2147483644
  FLAGS = (IOK,NOK,POK,READONLY,PROTECT,pIOK,pNOK,pPOK)
  IV = 1
  NV = 1
  PV = 0x70c924 "1"
  CUR = 1
  LEN = 0
SV = PVNV(0x236c070) at 0x23a3130
  REFCNT = 1
  FLAGS = (IOK,NOK,POK,pIOK,pNOK,pPOK)
  IV = 1
  NV = 1
  PV = 0x237c4f0 "1"\0
  CUR = 1
  LEN = 10
SV = PVIV(0x238f730) at 0x2391260
  REFCNT = 1
  FLAGS = (IOK,POK,pIOK,pPOK)
  IV = 1
  PV = 0x23da7b0 "1"\0
  CUR = 1
  LEN = 10

変数に代入せずにダンプした場合と代入してダンプした場合を比べて異なる点は

  • FLAGSに READONLY, PROTECT がつかなくなった
  • PVが指す文字列バッファの内容が変わった
    • NULL終端文字列が含まれている文字列になった
    • LEN(PVに割り当てられたバイト数)も 0 -> 10 になった

といった感じです。

READONLY, PROTECT がつかなくなるのは変更可能な変数に代入した影響でしょう。
となるとperl本体のコードを読まないと断言はできませんが、PVが指す文字列バッファの内容が変わるというのが変数に代入すると真偽値としての性質を失うということではないでしょうか。
変数に代入する前はPVの指すアドレスの先に真偽値としての性質を表す値があるのだが、変数に代入することでPVが指す文字列が普通の文字列になってしまうことで3番目に出力したような普通の値と区別がつかなくなってしまう、というようになっていそうです。

5.36

次に同じコードを5.36で実行してみた場合のDumpした結果を見ていきます。

SV = PVNV(0xca1030) at 0x9537e0
  REFCNT = 2147483644
  FLAGS = (IOK,NOK,POK,IsCOW,READONLY,PROTECT,pIOK,pNOK,pPOK)
  IV = 1
  NV = 1
  PV = 0x727be4 "1" [BOOL PL_Yes]
  CUR = 1
  LEN = 0
SV = PVNV(0xca1070) at 0xcd3ba0
  REFCNT = 1
  FLAGS = (IOK,NOK,POK,IsCOW,pIOK,pNOK,pPOK)
  IV = 1
  NV = 1
  PV = 0x727be4 "1" [BOOL PL_Yes]
  CUR = 1
  LEN = 0
SV = PVIV(0xcc86e0) at 0xcd3f48
  REFCNT = 1
  FLAGS = (IOK,pIOK,pPOK)
  IV = 1
  PV = 0xcb14f0 "1"\0
  CUR = 1
  LEN = 10

5.36 の場合、変数に代入してもPVが指す文字列バッファは変わっていません。
また、PVが指す文字列バッファが真値であることを示す PL_Yes であるということも表示されています。

というわけで、

  • 真偽値にはPVの指すアドレスの先に真偽値としての性質を表す値がある
  • 5.34 以前は真偽値に変数を代入することでPVが指す文字列が普通の文字列になってしまっていた
  • 5.36 からは真偽値を変数に代入してもPVの指すアドレスが変わらなくなったので、真偽値であると明確に区別できるようになった

ということのようです。

forループの繰り返しごとに複数の要素を参照する構文の追加

for文の括弧内にレキシカル変数を列挙することで複数の要素に対して反復処理を行えるようになりました。

use v5.36;
no warnings qw( experimental::for_list );

my %hash = (
    a => 1,
    b => 2,
);

for my ($key, $value) (%hash) {
    say "$key => $value";
}
b => 2
a => 1

keys を使わずに安全にハッシュをイテレーションできて便利になると思います。

my %hash = (
    a => 1,
    b => 2,
);

for my ($key) (keys %hash) {
    say "$key => $hash{value}";
}

もちろん一度に3要素以上の繰り返しも可能です。

my @points = (0, -3, 5, 6, 7, 9);
for my ($x, $y, $z) (@points) {
    ...
}

この機能は実験的な機能です。
使用した際に発生する警告を抑制するには no warnings qw( experimental::for_list ) が必要です。

try-catch 構文に finally block が追加

5.34 で追加された try-catch 構文ですが、try, catch ブロックが実行されたあとに実行する処理が書ける finally ブロックも書けるようになりました。

use v5.36;
use experimental 'try';

try {
    die 'hoge';
    say "Success";
}
catch ($e) {
    say "Failure";
}
finally {
    say "Regardless";
}

finally を書くことは頻繁にはないと思いますが、他の言語や Try::Tiny などで書けていたのに use feature 'try'; では書けないのもいまいちなので良い改善かなと思います。

defer 構文の追加

スコープを抜けるときに後で実行される構文です。

use v5.36;
use experimental 'defer';

{
    say '1';
    defer { say '3'; }
    say '2';
}
1
2
3

今までScope::Guard などを使って書いていた処理を簡単に書けるといった感じです。

Go の defer 構文とは実行タイミングや変数の扱いなど挙動が違う点があるのでその点は気をつけたほうが良さそうです。*2

新しい組み込み関数の追加とそれらの組み込み関数をインポートする仕組みの追加

builtin というコアモジュールが追加され、既存の組み込み関数とは別にインタプリタから完全修飾名ならいつでも呼ぶことができるユーティリティ関数がいくつか追加されました。

say "Reference type of arrays is ", builtin::reftype([]); # Reference type of arrays is ARRAY

また、 builtin モジュールの関数は use 文に import パラメータとして列挙することで直接インポートすることができます。

use builtin 'reftype';
say "Reference type of arrays is ", reftype([]); # Reference type of arrays is ARRAY

この仕組みができたことで、よく使うけどコアモジュールからインポートして使っていた様々なユーティリティ関数や、新しい関数を組み込み関数として追加しやすくなりました。

現状、この組み込み関数周りの仕組み及びこれらの組み込み関数は実験的機能の扱いです。
使用した際に発生する警告を抑制するには no warnings qw( experimental::builtin ) が必要です。

builtinモジュールに実装されている組み込み関数を紹介します。

builtin::trim

引数の文字列の先頭、末尾の空白及び改行をすべて削除する関数です。

use v5.36;
no warnings 'experimental::builtin';
use builtin qw( trim );

print trim "   hello!\n"; # hello!

微妙な使い分けはありそうですが chomp の上位互換になりそうです。

builtin::indexed

引数のリストをリスト内の各要素の順番と各要素のペアのリストにしたものを返す関数です。

use v5.36;
no warnings 'experimental::builtin';
use builtin qw( trim indexed );
use Data::Dumper;

warn Dumper [ indexed 'a' .. 'c' ]; # [ 0, 'a,', 1, 'b', 2, 'c' ]

forループの繰り返しごとに複数の要素を参照する構文と使うとforループの中でindexと配列の要素の参照が楽になりそうです。

for my ($index, $value) (indexed @array) {
    ...
}

builtin::true, builtin::false, builtin::is_bool

true と false はそれぞれ真値と偽値を返し、 is_bool は真偽値として作られた値かどうかを調べます。
true、false で返される値は !!1!!0 で返される値と同じです。
これらの true, false を使わなくてもperlは今まで通りどんな値でも真偽値として評価しようとしてプログラムを動作させるわけですが、「真偽値が安定して追跡できるようになった」でも述べたとようにこれらの関数で返された真偽値は内部的に真偽値として作られたと識別することができる値なので、他の言語との相互運用やデータ型のシリアライゼーションが行いやすくなります。

use v5.36;
no warnings 'experimental::builtin';
use builtin qw( true false is_bool );
use Devel::Peek qw( Dump );

Dump true;
Dump !!1;
SV = PVNV(0x18e9030) at 0x9537e0
  REFCNT = 2147483644
  FLAGS = (IOK,NOK,POK,IsCOW,READONLY,PROTECT,pIOK,pNOK,pPOK)
  IV = 1
  NV = 1
  PV = 0x727be4 "1" [BOOL PL_Yes]
  CUR = 1
  LEN = 0
SV = PVNV(0x18e9030) at 0x9537e0
  REFCNT = 2147483644
  FLAGS = (IOK,NOK,POK,IsCOW,READONLY,PROTECT,pIOK,pNOK,pPOK)
  IV = 1
  NV = 1
  PV = 0x727be4 "1" [BOOL PL_Yes]
  CUR = 1
  LEN = 0
use v5.36;
no warnings 'experimental::builtin';
use builtin qw( true false is_bool );
use Test::More;

ok is_bool true;
ok is_bool !!1;
ok !is_bool 1;

done_testing;
ok 1
ok 2
ok 3
1..3

builtin::weaken, builtin::unweaken, builtin::is_weak

それぞれ弱参照を作る関数、弱参照になっている参照を通常の参照に戻す関数、弱参照かどうかを判定する関数です。
元々 Scalar::Util にあった関数を持ってきた形になります。

builtin::blessed, builtin::refaddr, builtin::reftype

それぞれパッケージと紐付いている参照かどうか、参照のアドレス、参照の種類を返す関数です。
これらも元々 Scalar::Util にあった関数を持ってきた形になります。

builtin::ceil, builtin::floor

それぞれ引数の数値を切り上げた数値、切り捨てた数値を返す関数です。
これらは元々 POSIX にあった関数を持ってきた形になります。

Perl5.36 ができるまで

Perl5.36 が完成するまでにPerl開発チームでも大きな変化がありました。

1つ目は従来の pumpking に変わってPSC(Perl Steering Council)と呼ばれる、コアチームから選ばれた3人のメンバーから構成される運営評議会によって開発が進められたことです。
Perl5.34 もPSCの時代にリリースされましたが、PSCが発足したときには既にほとんどの開発が終了していました。また、そのころはPerl7などPerlの将来をどうするかといった問題などが残っており、具体的な変更よりもそちらの方が主な仕事になっていました。
対してPerl5.36ではPSCのメンバーによって開発プロセスやスケジュールが管理されるようになりました。
PSCのメンバーがうまく連携して開発を進めた結果、Perl5.36では過去のバージョンと比べてより多くの機能の追加や改善が行えたのではないかと思います。

2つ目はRFCが制定されたことです。言語の変更を提出する正式なプロセスとして昨年導入され、Perl5.36 の提案を管理するために使用されました。
結果、かなり透明性が高い状態で提案を管理できているようです。
PerlのRFCはこちらのリポジトリで見ることができます。

まとめ

このように 5.36 では以前と比べて大きめな変更がたくさん入っています。
ここでは書いていない変更もいろいろあるので気になった方はperldeltaを読んでみてください。

参考

*1:Perl Steering Committee (PSC) #015 Meeting Notes - nntp.perl.orgより 将来的にPerl7をリリースするときもPerl7の機能を use v7.0 で有効にすることになるので普及させたいそうです

*2:Shogoさんのこちらの記事で詳しくまとめられています