Masteries

技術的なことや仕事に関することを書いていきます.

Class::Accessor::Typedで独自の型を使う

Class::Accessor::LiteやClass::Accessor::Lite::Lazyのように使えて, 型の恩恵を受けられる, Class::Accessor::Typedというモジュールを昔書きました.

metacpan.org

詳しくは, 昔Kichijoji.pmで紹介した時に使った資料をまとめたものがあるので, こちらをご覧頂ければと思います.

papix.hatenablog.com

このモジュールは大変便利なんですが, 1つ課題がありました. それは独自の型をうまくいい感じに使う方法が当時の自分には思い浮かばなかった... ということです.

例えば, 次のような Types.pm があり, ここで「正の整数」であるPositiveIntという型を定義していたとします.

package Types;
use strict;
use warnings;

use Mouse::Util::TypeConstraints;

subtype 'PositiveInt',
    as 'Int',
    where { $_ > 0 },
    message { '0以上の整数でなければなりません' };

Class::Accessor::TypedでこのPositiveIntを使うには, 常にTypesを一緒にuseしないといけません(そうしなければ, Class::Accessor::TypedがPositiveIntの定義を見つけられないからです).

package Pkg;
use strict;
use warnings;

# これがないと, 次のようなエラーが発生する:
# 'baz': Validation failed for 'PositiveInt' with value 1 at
# ...
use Types;

use Class::Accessor::Typed (
    rw => {
        baz => 'PositiveInt',
    },
    new => 1,
);

1;

ちょっと面倒ですね. この辺りで悩んでいて, プロダクト開発に導入するにあたって悩んでいたのですが, ふと思いつきました. それは, 次のようなパッケージを用意して, これを使うようにするというアイデアです:

package MyClassAccessorTyped;

use Types;
use Class::Accessor::Typed;

sub import {
    goto \&Class::Accessor::Typed::import
}

1;

こうすると, MyClassAccessorTyped をuseするだけで事足りるようになります(use Types;しなくても意図通りに動作する).

package Pkg;
use strict;
use warnings;

use MyClassAccessorTyped (
    rw => {
        baz => 'PositiveInt',
    },
    new => 1,
);

1;

本来はClass::Accessor::Typed単体で綺麗に扱えるようにするべきなのでしょうが, 今のところかっこいい解決策が思いついていないので, この手で回避するのがいいのではないか, と思っています. ちょっとダサい(?)感じもしますが, Class::Accessor::LiteやClass::Accessor::Lite::Lazyで型の恩恵を受けられるのはそのダサさ(?)を超えるメリットがある... と思っています. この機会に是非Class::Accessor::Typedをお試し頂ければ幸いです.

また, Class::Accessor::Typedから独自の型をいい感じに使えるようにする問題について, 良いソリューションがありましたら, TwitterやPull Requestでご意見をお寄せ頂けると幸いです. よろしくおねがいします.

小ネタ: Perlのメソッド呼び出しをModule::Spy風にモックする

Perlでテストを書いているとき, 例えば外部のAPIを叩くメソッドをモックしたい, という気持ちになることがあります. 選択肢としてはTest::Mock::GuardやModule::Spy, 最近ならTest2::Tools::Mockあたりが選択肢になるでしょうか.

metacpan.org

metacpan.org

metacpan.org

「単純にモックしたい」という用途なら, 上記に挙げた3つのモジュール全てがその望みを叶えてくます. ただ, 「モックしたメソッドに渡った引数もチェックしたい」のであれば, Module::Spyが最適でしょう.

use DDP;
use Module::Spy;

my $spy = spy_on('Hogehoge', 'hoge_method')->and_returns(undef);

Hogehoge->hoge_method(args1 => 'a', args2 => 'b');

p $spy->calls_most_recent;
# \ [
#     [0] "Hogehoge",
#     [1] "args1",
#     [2] "a",
#     [3] "args2",
#     [4] "b"
# ]

package Hogehoge;

sub hoge_method { ... }

一方で, この時に hoge_method に渡った args1 をテストしたい... という場合は, 次のように書く必要があります.

use DDP;
use Module::Spy;

my $spy = spy_on('Hogehoge', 'hoge_method')->and_returns(undef);

Hogehoge->hoge_method(args1 => 'a', args2 => 'b');

my (undef, %args) = $spy->calls_most_recent->@*;
is $args{args1}, 'a';

Module::Spyは配列リファレンスを返し, かつその最初にはオブジェクト or パッケージ名の文字列が格納されているため, その手当も必要です. 数度ならいいですが, 都度都度こういった手当をするのは少々手間です. というわけで次のような, Module::Spy風のモジュールを使って処理するのはどうでしょうか.

package Test::Mock::Hogehoge;
use strict;
use warnings;
use utf8;

use Exporter 'import';

use Test2::Tools::Mock qw(mock);

our @EXPORT_OK = qw(mock_hogehoge);

sub mock_hogehoge {
    my $guard = Test::Mock::Hogehoge->new();
    $guard->{guard} = mock 'Hogehoge' => (
        override => [
            hoge_method => sub {
                my (undef, %args) = @_;
                $guard->_push_args(\%args);
            },
        ]
    );

    return $guard;
}

sub new {
    my ($class) = @_;

    bless { args => [] }, $class;
}

sub _push_args {
    my ($self, $args) = @_;

    push $self->{args}->@*, $args;
}

sub calls_all {
    my ($self) = @_;
    return $self->{args};
}

sub called {
    my ($self) = @_;
    return scalar $self->calls_all->@* == 0 ? !!0 : !!1;
}

sub calls_most_recent {
    my ($self) = @_;
    return $self->called ? $self->calls_all->[-1] : undef;
}

sub calls_first {
    my ($self) = @_;
    return $self->called ? $self->calls_all->[0] : undef;
}

sub calls_reset {
    my ($self) = @_;
    $self->{args} = [];
}

1;

これなら, 次のように書けて比較的ラクではないでしょうか. インターフェイスや使い方もModule::Spyに準拠しているので, 覚えることを増やさずにテストを簡単に書けて嬉しくなりそうです.

use Test::Mock::Hogehoge qw(mock_hogehoge);
use Test::More;

my $mock = mock_hogehoge;

Hogehoge->hoge_method(args1 => 'a', args2 => 'b');

is $mock->calls_most_recent->{args1}, 'bb';

テストを書くのは大事, とわかりつつ, ダラダラ書くのは大変という気持ちもあると思っていて, 言語を問わずこういう感じで少しでも短く, 簡単に書けるような取り組みをしていくのが大事かな, と思っています. 小粒すぎる技ではありますが, 今後もおもしろいアイデアが浮かんだらブログに書いていこうと思いました.

小ネタ: Gitでコミットメッセージ書いてる最中にコミットを取りやめる

だいぶ昔のツイートなのですが, さっき知ったので忘備録的にブログに書いておきます.

CLIでGitを操作してて, commit logはnvimで書いてるんですが, たまにgit commit した後に「あ, commitするbranch間違えた...!」みたいに気づくことがあって困ることがありました.

mattnさんのツイートにもあるように, :cq で閉じると,

hint: Waiting for your editor to close the file... error: There was a problem with the editor 'nvim'.
Please supply the message using either -m or -F option.

となって, コミットすることなくnvimを閉じることができます.

Perl 5.35.7 の builtin を試してみる

この記事は, 「Perl Advent Calendar 2021」の22日目の記事です.

qiita.com

昨日はtecklさんの「Perlアプリケーション + AWS Lambda + API GatewayでWebhookを受ける」でした.

qiita.com

Perl 5.35.7 で builtin を試してみる

metacpan.org

Perl 5.36に向けた開発版の現時点での最新版, Perl 5.35.7 では, builtinというユーティリティを提供するプラグマが提供されています. 特に注目すべきはtrue, false, isboolの提供でしょう. ということで, 実際にインストールして試してみます.

shogo82148.github.io

を参考に, 次のように5.35.7をインストールします:

plenv install 5.35.7 -Dusedevel

さあ, 試してみましょう. まず, そもそも truefalseは何を返すのでしょうか?

use strict;
use warnings;
use builtin qw(true false);
use feature qw(say);

my $true = true;
my $false = false;

say "'$true' / '$false'"; # => '1' / ''

このように, true!!1ないし!0(This gives an equivalent value to expressions like !!1 or !0.), false!!0ないし!1(This gives an equivalent value to expressions like !!0 or !1.)と同値となる値が得られるようになっています. Perlで真偽値を扱う時に, !!1!!0はよく使いますが, Perlの入門者からすると「これは一体...?」となりがちでした. これをtrue/falseで表せるのは大変嬉しいですね.

一方で, true/falseをJSON::XSなどでJSONに変換する時は注意が必要です:

use strict;
use warnings;
use builtin qw(true false);
use feature qw(say);
use JSON::XS qw(encode_json);

my $true = true;
my $false = false;

say encode_json({ true => $true, false => $false }); # => {"true":"1","false":""}

JSON::XSでは(デフォルトでは), \1true, \0falseとなるように変換してくれます. このため, \1 !=0 !!1, \0 != !!0であるため, true"1"に, false""に変換されてしまいます.

use strict;
use warnings;
use feature qw(say);
use JSON::XS qw(encode_json);

say encode_json({ true => \1, false => \0 }); # => {"true":true,"false":false}

このシンプルな解決策としては, JSON::Typesのboolを使うといった作戦がありそうです.

use strict;
use warnings;
use builtin qw(true false);
use feature qw(say);
use JSON::XS qw(encode_json);
use JSON::Types qw(bool);

my $true = true;
my $false = false;

say encode_json({ true => bool $true, false => bool $false }); # => {"true":true,"false":false}

また, このbuiltinプラグマでは, これまでScalar::Util で提供されていた blessed や weakref などが提供されるのも若干嬉しいポイントですね. 加えて, 真偽値かどうかの判定に使える, is_boolなども提供されています.

感想としては, 前述した通り!!1!!0というPerlで真偽値を扱う時の「よくある(が見慣れない)表現」をtrue/falseで書けるようになるのは嬉しいですね. 一方で, JSONにencodeする時の扱いは引き続き気をつける必要がありそうです. この辺りもいい感じになれば, 更に便利! という感じがしそうです.

明日は...

id:hitode909 さんです. よろしくおねがいします.

お詫びと御礼

何を言っても言い訳になるのですが, 最近頭痛が激しくて少し軽めの記事になってしまいました... 申し訳ありません. 元気になったらもうちょっと加筆します.

また, builtin の登場は会社のSlackで id:nanto_vi さんに教えてもらいました. この場を借りて御礼申し上げます.

Perlで @EXPORT を @EXPORT_OK に置き換える

この記事は「はてなエンジニア Advent Calendar 2021」の12日目の記事です.

qiita.com

昨日は id:cohalz の「distrolessのnonrootイメージを使おう」でした.

cohalz.co

関数のエクスポート

Perlのパッケージには, "エクスポート"という概念があります. 例えば, 次のようなモジュールがあったとしましょう:

package MyPkg;

sub f1 { ... }
sub f2 { ... }
sub f3 { ... }

パッケージMyPkgに定義されたf1, f2, f3の関数をパッケージの外から呼び出すには, 通常次のように呼び出す必要があります:

use MyPkg;

MyPkg::f1();
MyPkg::f2();
MyPkg::f3();

いちいち MyPkg を書くのは面倒, ということで用意されたのが Exporter モジュールです.

metacpan.org

このモジュールを使うと, グローバル変数@EXPORT自動的にエクスポートする関数を, @EXPORT_OKエクスポート可能な関数 を, それぞれ定義することができます. 例えば, MyPkgExporter を使って, 次のように書き換えたとしましょう:

package MyPkg;

use Exporter 'import';
our @EXPORT = qw(f1 f2);
our @EXPORT_OK = qw(f3);

sub f1 { ... }
sub f2 { ... }
sub f3 { ... }

この場合, f1, f2 の関数は自動的にエクスポートされるため, 次のようにして呼び出すことができます:

use MyPkg;

f1();
f2();
f3(); # 関数 f3 は @EXPORT で指定していないため呼び出せない(存在しない関数を呼び出したことになる)

f3 を呼び出したい時は, 次のようにMyPkguseするときに, 「この関数を利用します」と指定しなければなりません:

use MyPkg qw(f3);

f3(); # use するときに, f3 を呼び出せるよう設定しているので呼び出せる

@EXPORTの問題点

@EXPORT@EXPORT_OK, どう使い分けるべきでしょうか. 先程の例では, Exporterモジュールを使って関数をエクスポートしているパッケージが, MyPkgだけだったので, どちらでも問題ないように思えます. しかし, 複数のパッケージでExporterモジュールを使い, @EXPORTで関数をエクスポートしていると, どうなるでしょうか.

use MyPkg1;
use MyPkg2;
use MyPkg3;

exported_function();

パッと見で, 「exported_functionはどのパッケージで定義されているんだ...?」となりますよね. このため, Perl::Critiでも Perl::Critic::Policy::Modules::ProhibitAutomaticExportation というルールが用意されていて, @EXPORT を使って関数をエクスポートしようとすると警告してくれるようにすることができます.

metacpan.org

@EXPORT を @EXPORT_OK に置き換える

これを自動的に置き換えるにはどうすればいいでしょうか. 今回はPerlの静的解析ツールであるところのPPIを使って解決しました:

metacpan.org

use strict;
use warnings;

my $pkg = 'Foo';
my $path = 'lib/Foo.pm';

# $pkg においてエクスポートされている関数の名前を取得する
my $exported_function_names = get_exported_function_names($pkg);

# `git grep` を使って, $pkg を使っているファイルを探す
my @files_using_exported_function = split /\n/, `git grep --name-only --word-regexp 'use $pkg;' t/`;

# `replace_file` で, $pkg を使っているファイルについて利用する関数を明示するよう書き換える
replace_file($_, $exported_function_names) for @files_using_exported_function;

# $pkg の @EXPORT を @EXPORT_OK に書き換える
replace_export_ok($path);

# スペシャルサンクス: id:mackee_w & id:xtetsuji
# この関数で行っている, 変数でパッケージ名を指定してグローバル変数を読み出す方法がわからなかったのでアドバイス頂きました
sub get_exported_function_names {
    my $pkg = shift;
    no strict qw(refs);
    return [ @{ $pkg . '::EXPORT' } ];
}

sub replace_export_ok {
    my ($path) = @_;

    my $document = PPI::Document->new($path);

    my $export = $document->find(
        sub { $_[1]->isa('PPI::Token') && $_[1]->content eq '@EXPORT' }
    );

    return undef unless $export;

    $export->[0]->set_content('@EXPORT_OK');
    $document->save($path);
}

sub replace_file {
    my ($path, $exported_function_names) = @_;

    my $document = PPI::Document->new($path);

    # $exported_function_names に存在する関数のうち,
    # $path で実際に使っているものを探す
    my $used_exported_function_names = $document->find(
        sub { $_[1]->isa('PPI::Token') && any { $_[1]->content eq $_ } $exported_function_names->@* }
    );

    return undef unless $used_exported_function_names;

    my $unique_used_exported_function_names = [ uniq sort map { $_->content } $used_exported_function_names->@* ];

    # $pkg を use している部分を探して...
    my $use_statement = $document->find(
        sub { $_[1]->isa('PPI::Statement::Include') && $_[1]->module eq $pkg }
    )->[0];

    my $package_word = $use_statement->find(
        sub { $_[1]->isa('PPI::Token::Word') && $_[1]->content eq $pkg }
    )->[0];

    # `use $pkg;` を `use $pkg qw( ... );` のように書き換える
    # ... は $path で実際に使っているエクスポートされた関数の名前になる
    $package_word->insert_after(
        PPI::Token::Word->new(sprintf(" qw(%s)", join(' ', $unique_used_exported_function_names->@*)))
    );

    $document->save($path);
}

この例では, lib/Foo.pm にあるパッケージFooにある@EXPORT@EXPORT_OKに置き換えています. lib/Foo.pmだけでなく, Gitのgit grepを使って, 実際にFooを使っているファイルの中身も書き換えて, 置き換え後も正しくテストが通るようにしています. 実際にとあるPerlプロダクトで試してみましたが, 95%くらいはこの素朴なスクリプト(をベースにしたスクリプト)で一括置換することができました.

実は今回始めてPPIを使ったのですが, ドキュメントとにらめっこして1時間ちょっとくらいで素朴な置き換えスクリプトが実装できて便利でした(ので, おそらくもっと良い書き方はありそうです). PPIを使うことで, こういった細かい気になりポイントをガッとリファクタリング出来ることがわかったので, これからも良いアイデアが浮かんだらやっていきたいと思いました.

明日の担当は...

id:Windymelt さんです. よろしくおねがいします.

blog.3qe.us