Mobile Factory Tech Blog

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

Perl5.38の変更点

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

今年も7/2にPerlの最新安定バージョンである5.38がリリースされたので新機能や変更点についてまとめます。
5.38 はかなり変更点が多いですが、ニッチな機能に対する変更も多いので影響の大きそうな箇所だけ知りたい方は最初の方だけ読んで頂くといいと思います。

重要な変更点

class構文の追加

実験的機能としてですが、ついに Perl にclass構文が追加されました。

次のような構文になります。

 use v5.38;
 use experimental 'class';

 class Point;

 field $x :param = 0;
 field $y :param = 0;

 method move($dx = 0, $dy = 0) {
   $x = $dx;
   $y = $dy;
 }

 method print {
   say "x: $x, y: $y";
 }

 my $p = Point->new(x => 3, y => 5);
 $p->print(); # x: 3, y: 5
 $p->move(2, 4);
 $p->print(); # x: 2, y: 4
  • クラスは class 文で宣言します。使い方は package と同じ感じで、ブロックでスコープを作ったりバージョンを指定することもできます。 class 文を利用するとコンストラクタである new メソッドは自動的に生成されますが、自前で書くことはできないです(実行時にエラーになる)
  • インスタンス変数は field 文で宣言します。スカラだけでなく配列、ハッシュの変数も宣言可能です。従来よく使われていたハッシュリファレンスを bless したクラスと違って、宣言されたインスタンス変数はパッケージ外部から直接参照することはできません
  • メソッドは method 文で宣言します。 method 内では field で宣言されたインスタンス変数を参照することと、現在のオブジェクト自身を指す暗黙的変数 $self を参照することができます。他はサブルーチンと同じような仕様でシグネチャの定義や匿名メソッドを作ることも可能です。ちなみに $self からインスタンス変数を参照することはできません(Java の this.x のようにインスタンス変数を参照することは不可能)

class, field には attribute を設定することが可能で、これにより上記の構文だけでは実現できない機能を実現しています。

上記で既に使用していますが、field:param attribute を設定するとその変数名の名前付き引数をコンストラクタに渡して初期化できるようになり、 field の初期値が定義されていない場合は名前付き引数が渡されないとエラーが発生するようになります。

継承は class:isa attribute を設定することでできます。

 use v5.38;
 use experimental 'class';
 
 class People {
   field $name :param;
 }
 
 class User :isa(People) {
   field $id :param;
 }

親クラスは1つしか指定することができません。また、親クラスのインスタンス変数は直接参照できません。

従来のクラスと違う点をまとめると以下のようになります。

  • 継承できるクラスは1つだけ
  • インスタンス変数はパッケージ外部から直接参照できない
  • bless しているリファレンスがないので従来のオブジェクトとは異なる判定をされることがあるので注意
    • Scalar::Util::reftypebuiltin::reftype では OBJECT という値が返ってきます
    • Scalar::Util::blessedbultin::blessed は同じように使えます

現在実装されている機能は主に上記のものだけです。 Moose などであったRole機能がなかったり、コンストラクタに渡された引数を操作できなかったり、アクセサの自動生成ができないため、まだ本格的に class 構文をプロダクトで利用するのは難しそうです。

一応今後の開発方針としては

  • Role機能の実装
  • コンストラクタが呼び出されたあとに呼び出される ADJUST ブロックでコンストラクタに渡された引数を受け取れるようにする
  • アクセサを自動生成する attribute の実装
  • メタプログラミングAPIの実装
  • コアモジュールをclass構文に対応させる

ということが決まっているようなので、次以降のバージョンに期待ですね。

モジュールの最後で 1; を書かなくて良くなった

モジュールの最後で真値を返さなくても良くなる機能 module_true が追加されました。
これを使うことでモジュールファイルの最後で 1; を書く必要がなくなります。

 # Hoge.pm
 package Hoge;
 use feature 'module_true';
 
 sub do_something {}
 
 # main.pl
 use Hoge; # 1; を書いていなくてもロードに成功する

module_trueuse v5.38 することでも有効になるので、積極的に use v5.38 していきましょう!

  # Hoge.pm
  package Hoge;
  use v5.38;
  
  sub do_something {}
  
  # main.pl
  use Hoge;

設定ファイルなどで稀に任意の値をファイルの最後で返したいこともあると思いますが、そのような場合は use v5.38 しないか no feature 'module_true' などとすると従来と同じように利用できます。

構文エラー発生後パースを続けないようになった

perl5.36以前では一度構文エラーが発生した後もパースを続けていましたが、perl5.38からは変数名や定数名を間違えたときのエラーを除いて一度構文エラーするとそこでパースが止まるようになりました。

例えば次のような構文エラーになるコードをperl5.36で実行すると

 use v5.36;
 
 my $hash = +{ a => 10 ;
 
 my $str = 'aaaaa';
 
 my $str2 'bbbbb';

次のようなエラーが発生します。

 String found where operator expected at compile_error.pl line 7, near "$str2 'bbbbb'"
    (Missing operator before 'bbbbb'?)
 syntax error at compile_error.pl line 5, near "my "
 Global symbol "$str" requires explicit package name (did you forget to declare "my $str"?) at compile_error.pl line 5.
 syntax error at compile_error.pl line 7, near "$str2 'bbbbb'"
 Missing right curly or square bracket at compile_error.pl line 7, at end of line
 Execution of compile_error.pl aborted due to compilation errors.

エラーが5行目と7行目で発生していて、無理にパースを続けているせいで Global symbol "$str" requires explicit package name など的外れなエラーメッセージもでてしまっていてわかりにくいです。

このコードをperl5.38で実行すると次のように最初の構文エラーしか発生しないようになっていて、構文エラー発生後パースを続けないようになったことがわかります。

 syntax error at compile_error.pl line 5, near "my "
 Execution of compile_error.pl aborted due to compilation errors.

変数名や定数名などを間違えたときのエラーは変わらず複数あっても出続けるので、そのような修正は今までと同じようにできます。

 use v5.38;
 
 my $str = 'Hello';
 say $srt;
 
 my $str2 = 'World';
 say $srt2;
 Global symbol "$srt" requires explicit package name (did you forget to declare "my $srt"?) at compile_error_2.pl line 4.
 Global symbol "$srt2" requires explicit package name (did you forget to declare "my $srt2"?) at compile_error_2.pl line 7.
 Execution of compile_error_2.pl aborted due to compilation errors.

他の言語も基本的に複数箇所でコンパイルエラーが起きるコードを実行しても最初のエラーで止まるので、今までのperlが特殊だった感じがありますし、
途中でコンパイルエラーになるコードをパースし続けても変なパースをしてわかりにくいエラーメッセージが出たりセグフォになる可能性もあるので、いい変更だと思います。

廃止予定になった機能

次の機能が廃止予定になり、利用していると警告が発生するようになりました。

パッケージセパレータ '

パッケージの区切り文字は通常 :: を使いますが、Perl4の頃は ' を区切り文字に利用していてその流れでPerl5でもパッケージセパレータとして利用することができていました。
これがPerl5.42で廃止されます。
jcode.pl を利用しているCGIスクリプトなど、Perl4時代から運用しているコードがあれば将来かなり影響を受けることになりそうです。

switch構文、スマートマッチング演算子

Perl5.10で追加され、Perl5.18で実験的機能となったswitch構文とスマートマッチング演算子が失敗した機能とされ、Perl5.42で廃止されることが決まりました。

swtich構文は以下のような given, when などからなるswitch文のような構文です。

 use feature 'switch';
 given ($num) {
     when ($num < 50) { say 'yes' }
     default { say 'no' }
 }

スマートマッチング演算子は以下のように項のデータ型に基づいて比較を行う演算子で、例えば配列やハッシュが一致するかを調べたりすることができました。

my @ary = (0 .. 10);
my @ary2 = (0 .. 10);
if (@ary ~~ @ary2) {
  say 'Same array';
}

廃止予定機能使用時の警告にサブカテゴリが追加

廃止予定の機能を使っている警告は no warnings 'deprecated'; で抑制することができます。
しかしこれだと廃止予定の機能を使用したの際の警告をすべて抑制してしまうので、no warnings 'deprecated::${機能名}'; というように機能ごとに警告を抑制することができるようになりました。

例えばこのようなコードだとスマートマッチング演算子以外の廃止予定機能を使用したときも警告がでなくなります。

use v5.38;
no warnings 'deprecated';
use v5.10;
if ( [1 .. 3] ~~ [1 .. 3] ) {
    print "match";
}

スマートマッチング演算子利用のサブカテゴリの警告のみを抑制よるすることでスマートマッチング演算子だけ使用したときの警告がでなくなります。

use v5.38;
no warnings 'deprecated::smartmatch'; # Downgrading a use VERSION declaration to below v5.11 is deprecated, and will become fatal in Perl 5.40
use v5.10;
if ( [1 .. 3] ~~ [1 .. 3] ) {
    print "match";
}

細かな改善やニッチな変更点

try-catch 構文、defer構文の改善

  • finally, deferブロック内で goto 文が使えませんでしたが、finally, defer ブロック内にジャンプする場合は使えるようになりました
  • finally, defer から離れる制御フロー(return, gotoなど)はコンパイル時にエラーになるようになりました
 use v5.38;
 use experimental 'try';
 
 try {
     die if rand(1) > 0.5;
 }
 catch ($e) {
     say "caught error: $e";
 }
 finally {
     say "finally";
     return; # Error: Can't "return" out of a "finally" block
 };

%{^HOOK} API の導入

perlのコア関数の中にはオーバーライドすることが難しい関数があるため、そのような関数の前後に呼ばれるコールバック関数を登録することのできるAPIが追加されました。
現状 require のみ対応しており、今後他のオーバーライドすることが難しい関数にもフック関数を登録できるようにする予定らしいです。

 ${^HOOK}{require__before} = sub {
   my ($filename) = shift;
   warn "require before: $filename";
 };
 
 ${^HOOK}{require__after} = sub {
   my ($filename) = shift;
   warn "require after: $filename";
 };
 
 use v5.38;
 require List::Util;
 
 say List::Util::sum(1 .. 10);
require before: List/Util.pm at override_require_hook.pl line 3.
require before: strict.pm at override_require_hook.pl line 3.
...
require after: strict.pm at override_require_hook.pl line 8.
require after: List/Util.pm at override_require_hook.pl line 8.
55

コア関数をオーバーライドしたい場合は CORE::GLOBAL をコンパイル時にオーバーライドするのですが、それがAPIを利用してできるようになった感じだと思います。

 use v5.38;
 
 BEGIN {
   *CORE::GLOBAL::require = sub {
     my $file = shift;
     warn "require args: $file";
     CORE::require($file);
   };
 }
 
 use List::Util qw( sum );
 
 say sum 1 .. 10;

perl5380deltaでは require をオーバーライドするとスタックの深さが変わってモジュールの関数をエクスポートする処理で意図していないpackageにインポートしてしまう問題があると書いてあり、
実際に再現しようと上記のようなコードを用意してみましたが関数をエクスポートする処理(List::Util::sum)はちゃんと成功して再現できませんでした。
詳しい方がいらっしゃればぜひ教えていただきたいです。

@INCフックの改良

Perl で use, require などでモジュールをロードするとき、配列 @INC に格納されているディレクトリのリストの中にモジュール名に対応するファイルパスがないか検索するといった処理が実行されます。
@INC にはコードリファレンスやオブジェクトなどを入れることでモジュールのロード処理にフック処理を入れることができ、このフック処理を@INCフックと呼びます。

@INCフックにはフックが自身が @INC を修正するとセグフォや例外が発生する不具合があり、それが発生しないように修正されました。
また、次の機能が追加されました。

INCDIRメソッドの追加

新しいフックメソッド INCDIR が追加されました。
INCDIR メソッドが実装されたクラスのオブジェクトを @INC に追加してそのフックが実行されると、INCDIR メソッドが実行され返り値のリストが @INC に追加されます。

  package INCHooker {
  
    use v5.38;
  
    sub new($class, %args) {
      return bless +{ %args }, $class;
    }
  
    sub INCDIR {
      return ('/usr/local/lib/perl5', 'tmp');
    }
  
  }
  
  use v5.38;
  
  my $hooker = INCHooker->new;
  push @INC, $hooker;
  
  # エラーになるが、エラーメッセージを見ると
  #  Can't locate Ghost.pm in @INC (... INCHooker=HASH(0x55ecde25c5e8) /usr/local/lib/perl5 tmp)
  # というように @INC に INCDIR の返り値が追加されたことがわかる
  require Ghost

$INC による@INCのイテレーション制御

@INCフックの中ではインデックスが $INC に格納されるようになり、 $INC を書き換えると次にチェックされる@INCの要素は $INC の値の次の要素(undefの場合は0)になります。

例えば次のようなフックがある場合、requireで存在しないモジュールをロードしようとしたとき、@INCの要素を走査していって最後にフックが実行されるがフックの中で $INC がリセットされ、また先頭から@INCを走査する・・・といった処理を無限に繰り返すようになります。

 push @INC, sub {
   warn $INC;
   undef $INC;
 };
 
 require Ghost; 

@INCフックは非常にトリッキーな機能なのでプロダクトの開発で利用することはないと思いますが、Carmel などのライブラリで利用されています。
モジュールマネージャーなど特殊なモジュールロードをするようなコードを書いている人にとってはより効率的にモジュールをロードできるようになったりと、これらの機能強化は嬉しい変更になるのかもしれません。

サブルーチンシグネチャのデフォルト式の定義性論理和と論理和

サブルーチンシグネチャのデフォルト引数を //=, ||= でも指定することが可能になりました。
前者は引数が未定義値なら、後者は偽値ならデフォルト値が使われます。

 use v5.38;
 
 sub func($str //= 'World') {
   say "Hello, $str";
 }
 
 func(); # Hello, World
 func(undef); # Hello, World
 func('Anonymous'); # Hello, Anonymous

正規表現のパターン中のコードの埋め込みで楽観的評価が可能に

正規表現のパターンの中では (?{ code })(??{ code }) といった拡張構文でコードを埋め込むことが可能ですが、これらを使うとそのパターン全体での様々な最適化が無効になってしまいます。

そこで正規表現エンジンの最適化が無効化されない新しい拡張構文 (*{ ... }) が追加されました。
拡張構文中のコードが呼び出される回数が増えたり減ったりするかもしれないので挙動が不安定になる恐れがありますが、実行される回数は正規表現エンジンが動作する回数と一致します。
例えば通常の使用では O(N) のパターンが、 (?{ ... }) パターンを含むと O(N*N) になるところを (*{ ... }) に切り替えるとパターンは O(N) のままになるケースがあります。

 my $count1 = 0;
 ('a' x 10) =~ /(.*)(?{ $count1++ })[bc]/;
 say $count1; # 66
 
 my $count2 = 0;
 ('a' x 10) =~ /(.*)(*{ $count2++ })[bc]/;
 say $count2; # 11

コードの評価結果が正規表現として扱われる (??{ code }) に対応する (**{ code }) はまだ実装されていないです。

新しい正規表現変数 ${^LAST_SUCCESSFUL_PATTERN} の追加

現在のスコープで最後にマッチングに成功したパターンを利用したい場合は空パターンにすることで参照できていましたが、変数 ${^LAST_SUCCESSFUL_PATTERN} でも参照できるようになりました。

例えば最後にマッチングに成功したパターンにマッチする箇所を置換したい場合は置換対象のパターンを空にしていたところ、

 my $str = 'foofoo';
 if ($str =~ /foo/ || $str =~ /bar/) {
   $str =~ s//hoge/;
 }
 say $str; # hogefoo

${^LAST_SUCCESSFUL_PATTERN} を使うと次のように書き直すことができるようになり、コードが読みやすくなります。

 my $str = 'foofoo';
 if ($str =~ /foo/ || $str =~ /bar/) {
   $str =~ s/${^LAST_SUCCESSFUL_PATTERN}/hoge/;
 }
 say $str; # hogefoo

組み込み関数(builtin)の追加

export_lexically 関数の追加

現在コンパイル中のスコープにシンボルをエクスポートする関数です。
これによって builtin のレキシカルインポートの機能が builtin 以外のモジュールでも利用できるようになりました。

使い方は奇数番目の引数にシンボルの名前を指定して偶数番目の引数に対応するエクスポートしたい値のリファレンスを指定します。
値が変数の場合名前にシジルが必須で、変数の型と一致しなければなりません。値がサブルーチンの場合はなくてもよいです。

この関数はコンパイル時に呼び出されなければなりません。
通常この関数はモジュールのimportメソッド内で使われ、use文によって呼び出されます。

次のコードは関数 do_something と 変数 $hoge をレキシカルスコープにエクスポートするモジュールのコードと、それを use したコードです。

Hoge.pm

 package Hoge;
 
 use v5.38;
 use builtin qw( export_lexically );
 no warnings 'experimental::builtin';
 
 sub import {
   my $class = shift;
   export_lexically do_something => \&do_something,
                    '$hoge'      => \'hogehoge';
 }
 
 sub do_something {
   say 'Who am I?';
 }

main.pl

 use v5.38;
 {
   use Hoge;
   do_something(); # Who am I?
   say $hoge; # hogehoge
 }
 do_something(); # Undefined subroutine &main::do_something

2行目からのスコープ内のみに Hoge::do_something をエクスポートしていて、スコープから外れると未定義になることがわかります。

is_tainted 関数の追加

perlには汚染モードと呼ばれる外部から入力されたデータを汚染されたデータとしてチェックするモードがありますが(詳細はperlrun, perlsecを参照)、それが有効になっているときに値が外部から入力されたデータかどうかをチェックする関数です。

 use v5.38;
 use builtin qw( is_tainted );
 no warnings 'experimental::builtin';
 
 my $line = <STDIN>;
 if (${^TAINT}) {
   say is_tainted($line); # 1
 }

Scalar::Util の tainted を builtin に持ってきた形になります。

まとめ

一昨年までのPerl開発チームでの体制変更がいい影響を及ぼし続けているのか、今年もかなり大きな変更がありました。
特にクラス構文の追加やモジュール末尾の 1; が不要になったのは大きく、初心者が躓きやすいポイントや他言語出身の方が感じるとっつきづらさがどんどん減っていってると思います。
今後もPerlの進化が楽しみですね。

昨年度からperldoc.jpでperldocの翻訳がかなり進んでおり、この記事では書けなかったこともあるので詳しいことが気になった方はドキュメントの方もぜひ読んで見てください。