その手の平は尻もつかめるさ

ギジュツ的な事をメーンで書く予定です

YAPC::Asia Tokyo 2014でPerl::Lintについて喋りました

タイトルのとおりです.
スライドは以下です.



もうちょい詳しく話す予定だったんですが,冒頭のライブリリースに失敗するなどして出鼻をくじかれテンパってしまいました……
ちょっと詳しい話をすると,

  • ポリシー周り
    • 各ポリシーがトークンを受け取って,それを各々独自に走査して処理する
    • ポリシーはevaluate()というメソッドを持っていて,そこで解析処理をする.各ポリシーのevaluate()はそれぞれ同じ引数を受け取り,violationsを含むarray referenceを返す.ゆるふわなインターフェイス的思想.
  • フィルターもゆるふわなインターフェイス的思想になってて,filter()というメソッドを持ち,filterしたいポリシー名を含んだarray referenceを返す.
  • こんな感じで,新しいポリシーやフィルターを作りやすい!! と思ってたんだけどそうでもない気がする,後述.
  • とりあえず,開発のモチベーションとして,処理が高速でないと話にならんと思ったのでパフォーマンスの為にあえてダーティなコードを書いている部分がある
  • 一方でパフォーマンスとは関係の無いダーティなコードも書いている
  • あとまあ,Perl::Lintが速い場合と遅い場合があるのはわかっていて,スライドには割と恣意的なベンチの結果を載せている


あと,現状だと独自のポリシーとか独自のフィルターを書く為の敷居が高過ぎる (例えば,作法とかが一切わからん) という意見をもらったので,そこら辺を上手くサポート(チュートリアルとかスケルトンジェネレータとかで)したいなあというモチベーションになりました.何か良い意見あれば是非教えて下さい!


質疑応答は(覚えている限り)以下のとおりです.

Q. なんでAST使わないの?
A. Compiler::Parserがまだ不安定で,解釈できないコードが多かったため諦めた

Q. ASTじゃなくてToken使うことによるメリットってあるの?
A. よくわからない,無いのでは.手でToken走査して頑張るの本当に大変.ASTあるなら本当に使いたいケースは確かにある.

Q. Compiler::Lexer,SEGVとか多いイメージあるけど最近どう?
A. 最近では少なくなっているとは思います.でもまあ起きるには起きるんで……

Q. コントリビュータ募集って言ってたけど,どのへんに協力して欲しいの?
A. ぶっちゃけ僕が実装したくないポリシーを実装して欲しいんですけど,まあ無理な話なんでドキュメントとかで協力していただきたく存じます.

Q. Perl::LintにPerl::LintかけるとたくさんPerl::Lintのviolation出るけどこれって直るの?
A. 無理では……(ほんとうに難しい)

Q. C-Style forやめたら?
A. C-Style forの方が楽なんで許して欲しい (これは発表中にも喋ったんですけど,トークンの先読みとか後戻りとかを結構するので自在にインデックスをいじれたほうが良い)



YAPC会期中は常に会場のどこかしらにいますんで,何かPerl::Lint周りに興味がある方いらっしゃいましたらお話しましょう!

YAPC::Asia Tokyo 2014でPerl::Lintについて喋ったりします

おわび

Perl::Lint出来てません

本題

明日8月28日(木)から3日間に渡ってアジア最大のPerlのカンファレンスであるところのYAPC::Asia Tokyo 2014が開催されますね! 楽しみですね! 夜も眠れないですね!


さて,そのYAPC::AsiaでわたくしPerl::Lintという開発中のツールについて一席打たせて頂きます.
Perl::Lint - Yet Another Perl Source Code Linter - YAPC::Asia Tokyo 2014

  • そもそも静的解析とは何か
  • 静的解析して何が嬉しいのか
  • Perlにおける静的解析ってぶっちゃけどーなのか
  • Perl::Lintの構造とかポリシーとか実装とかってどうなってんのか
  • Perl::Lintにたっぷり詰まってるバッドノウハウについて
  • 理想の社会とは何か

みたいな話をする予定です.「Perl::Lintの話」と銘打ちましたが,コードの静的解析技術やその応用について言及する予定です.
どっこいあくまで予定です.まだ資料できてません.
資料できてませんが,必ず良い発表にするので皆さん是非足を運んでいただけたらと存じます.


資料できてないしPerl::Lintもできてないしで,別の意味で夜も眠れませんが,俺のこの熱量は本物です.
それではみなさん会場でお会いしましょう!!!!!!!

XSのABRTをキャッチしたい





以下の様なコードを実行するとABRTしてアプリごと死ぬという厳しい状況にぶち当たりました.
(なおこのバグはfix issue #45 · 93ff36f · goccy/p5-Compiler-Lexer · GitHubにて修正されている様子.@さんには頭が上がりません.ありがとうございます)


$SIG{ABRT} = sub {...};

という具合で書いてやればABRTを握れるのかな〜とか思って書いてみたら,この方法ではシグナルをトラップできない感じだったので,やむなく以下の様にforkを使ってなんとかすることにしました.
https://github.com/moznion/Perl-Lint-Playground/blob/5d9c23b59646dcc65fbb8d95e67368f6b02c3c01/lib/Perl/Lint/Playground/Service/Lint.pm

IO::Pipe便利!!!!!


さて皆さんこういう感じで,入力によってはXSがエラー吐いてどうにもならん時ってどうされてるんですかね?

Published “Perl::Lint Playground”

(またしても日本語は下にあります / Japanese follows English)


I published “Perl::Lint Playground”.
http://perl-lint.moznion.net/


You can try feature of Perl::Lint easier on this playground.
However this and Perl::Lint have not been completed yet, so possibly results are vague.
(e.g. Now, source code that was written in this playground is not file in spite of playground warns by policy of file path. I think there is more problems...)


For now, please play here and tell me your mind.
Enjoy!




Perl::Lint Playgroundを公開しました.
http://perl-lint.moznion.net/


ブラウザ上でPerl::Lintを試してみることができます.
まあ,みなさんもご存知の通りPerl::Lintは未完成ですので,このプレイグラウンドの結果もあんま信用しないほうが良いです.
(例えば,プレイグラウンドで試す為のソースコードはファイルではありませんが,現状ではファイルにまつわるポリシーによって警告されるなどといった問題があります.多分他にも結構あるとおもいます)


とりあえず遊んでみて,僕にフィードバックなどをくれると嬉しいです.
よろしくお願いします.

Released Perl::Lint as underdevelopment

(日本語は下にあります / Japanese follows English)


I've just released Perl::Lint as underdevelopment.
https://metacpan.org/release/MOZNION/Perl-Lint-0.01_01


Development of Perl::Lint is in progress, but I need feedback about this module.
Thus I released it on CPAN to simplify installation in spite of it is on the way.
(Thank you for your advice, @tomhukins and @)


Installation is easy.

$ cpanm --dev Perl::Lint


And usage is simple!

use Perl::Lint qw/lint/;
my $violations = lint(['path/to/file.pl']);


This module is unfinished as you know.
Please don't believe this and me!!

I'm looking forward to hearing your feedback.
Please send your mind to the GitHub Issues or @.

Enjoy!


P.S.
I'm going to talk about Perl::Lint at YAPC::Asia Tokyo 2014.
Let's talk me at there!!



Perl::Lintを開発版としてリリース致しました.
https://metacpan.org/release/MOZNION/Perl-Lint-0.01_01


Perl::Lintは目下のところ開発中のモジュールですが (まだ出来てねえのか!) ,そろそろユーザのフィードバックが欲しいなあとか,Tom Hukins氏や@氏らから「とりあえずリリースしちゃえよ! インストールしにくくてかなわん!」という意見を頂いたとか,そういった事情でunderdevelopment releaseを敢行したというのが背景です.


インストールは至って簡単です.

$ cpanm --dev Perl::Lint


使い方もシンプルです.

use Perl::Lint qw/lint/;
my $violations = lint(['path/to/file.pl']);


ご存知の通りこのモジュールは未完成です.
このモジュール (の吐き出す結果) と俺を信用しないでください.

フィードバックはGitHub Issuesに書いてもらえるか@まで直でメンション飛ばしてもらえると嬉しいです.

よろしくお願いします.


[追記]
そういや言い忘れてましたが,YAPC::Asia 2014というイヴェントでPerl::Lintの話をします.
会場でお会いしましょう!!!

先読みとautovivificationの話,あるいはマイクロオプティマイゼーションの話

Perlの話です.が,先読みの辺りはどの言語でも共通なのでは,という感じです.

追記

なんか先読み関係ない感じになってるのでコメント見ると良いです.この記事の情報は誤っているので後で書き直す.

ここから先は読まなくても良い

さて,配列を走査するような処理を書く時,「1個後のアイテムと現在のアイテムを比較したい」というようなニーズから,1個後のアイテムを読んでおきたいというような事があると思います.

ナイーブに実装するとしたら以下のようになるでしょう (C-Style Loopで書いている理由は以降の比較のためなので深く考えないで下さい).

my @array = (1..100);
for (my $i = 0; my $item = $array[$i]; $i++) {
    my $next_item = $array[$i+1];
    # do something...
}

このコードは,先読みした値を変数に格納しておいて,それを使いまわすというような効率的な書き方に変形する事が出来ます.

my @array = (1..100);
for (my $i = 0, my $next_item; my $item = $next_item || $array[$i]; $i++) {
    $next_item = $array[$i+1];
    # do something...
}

さてこの両者でベンチマークを取ってみると以下の様な感じになります.

use Benchmarks sub {
    my @array = (1..100);

    my $normal = sub {
        for (my $i = 0; my $item = $array[$i]; $i++) {
            my $next_item = $array[$i+1];
            # do something...
        }
    };

    my $read_ahead = sub {
        for (my $i = 0, my $next_item; my $item = $next_item || $array[$i]; $i++) {
            $next_item = $array[$i+1];
            # do something...
        }
    };

    +{
        normal     => $normal,
        read_ahead => $read_ahead,
    };
};
__END__
              Rate     normal read_ahead
normal     39711/s         --       -25%
read_ahead 52609/s        32%         --

速いですね! めでたしめでたし!

めでたくない場合

しかしながら走査するオブジェクトの構造が変わるとそうは問屋がおろしません.
以下の様なコードの場合を考えてみましょう.

use Data::Faker;
my $faker = Data::Faker->new;
my @array;
for (1..100) {
    push @array, +{
        name => $faker->name,
        age  => int rand(80),
    };
}

for (my $i = 0, my $next_item; my $item = $next_item || $array[$i]; $i++) {
    $next_item = $array[$i+1];
    $next_item->{name};
}

このコードは永遠に終わりません.無限ループします.

このコードで配列の最後の要素にアクセスしている時,$next_itemundefとなりますが,このundefに対してハッシュリファレンスのようにアクセスすると,undef{}という風に空のハッシュリファレンスに変化してしまいます (もちろん配列リファレンスで同じようなことを行なった場合も同様です).以下の様な感じ.

my $foo = undef;
$foo->{not_exists}; # <= この時点で$fooは{}になっている!

Perlでは空のハッシュリファレンスは真値であるので,論理和の評価は空ハッシュである$next_itemが返ってきてしまい,for文の条件が常に真として扱われてしまい無限ループしてしまうという訳ですね.

このように,走査対象の配列の中身がハッシュリファレンスで,なおかつ1個次の要素を見る必要がある場合,上手くいかずにハマる場合があるわけです.

そこで我々はどうすべきか

1. ローカル変数を1個作って,そこに$next_itemをコピーしてそっちにアクセスする
for (my $i = 0, my $next_item; my $item = $next_item || $array[$i]; $i++) {
    $next_item = $array[$i+1];
    my $_next_item = $next_item;
                                                                            
    # do something...
    $_next_item->{name};
}

このようにすると無限ループは起きないものの……

use Data::Faker;
use Benchmarks sub {
    my $faker = Data::Faker->new;
    my @array;
    for (1..100) {
        push @array, +{
            name => $faker->name,
            age  => int rand(80),
        };
    }

    my $normal = sub {
        for (my $i = 0; my $item = $array[$i]; $i++) {
            my $next_item = $array[$i+1];

            # do something...
            $next_item->{name};
        }
    };

    my $read_ahead = sub {
        for (my $i = 0, my $next_item; my $item = $next_item || $array[$i]; $i++) {
            $next_item = $array[$i+1];
            my $_next_item = $next_item;

            # do something...
            $_next_item->{name};
        }
    };

    +{
        normal     => $normal,
        read_ahead => $read_ahead,
    };
};
__END__
              Rate read_ahead     normal
read_ahead 19855/s         --       -11%
normal     22399/s        13%         --

先読みしたほうが遅い!!!! 意味無いじゃん!!!!
……まあそりゃそうですよねという感じ.

2. no autovivificationする

undef->{something}->[42]のようにアクセスするとそれぞれ空の{}[]という風になってしまうのが問題なので,その挙動を潰してやれば良い!

ということでautovivificationの出番です.
https://metacpan.org/pod/autovivification

no autovivificationという風に宣言してやると,そのレキシカルスコープでvivification,つまりundefを自動で空のハッシュリファレンスや配列リファレンスに変換する処理を無効にすることが出来ます.

no autovivification;
for (my $i = 0, my $next_item; my $item = $next_item || $array[$i]; $i++) {
    $next_item = $array[$i+1];
    # do something...
    $next_item->{name}; # <= ここでundefが変換されない!
}

ベンチマークの結果は以下のとおり

no autovivification;
use Data::Faker;
use Benchmarks sub {
    my $faker = Data::Faker->new;
    my @array;
    for (1..100) {
        push @array, +{
            name => $faker->name,
            age  => int rand(80),
        };
    }

    my $normal = sub {
        for (my $i = 0; my $item = $array[$i]; $i++) {
            my $next_item = $array[$i+1];
            # do something...
        }
    };

    my $read_ahead = sub {
        for (my $i = 0, my $next_item; my $item = $next_item || $array[$i]; $i++) {
            $next_item = $array[$i+1];
            # do something...
        }
    };

    +{
        normal     => $normal,
        read_ahead => $read_ahead,
    };
};
__END__
              Rate     normal read_ahead
normal     36885/s         --       -18%
read_ahead 44776/s        21%         --

おっ,速い!

まとめ

  • undefにリファレンスアクセスするといつの間にかundefが別のものになる
  • うかつに先読みしない
  • とは言え先読みしたい,なおかつ少しでもパフォーマンスが気になる場合はno autovivificationを使うのが良いのでは無いか.


という感じです

Time::SecondsのONE_MONTHとONE_YEARについて

Time::SecondsONE_MONTHONE_YEARを使う場合,本当にその方法で良いのかよく考えたほうが良いと思います.バグが出る可能性が高い気がします.

例えば以下の様な場合

use Time::Piece;
use Time::Seconds;

my $tp = localtime->strptime("2014-04", "%Y-%m");
say $tp->mon; # => 4
say $tp->ymd; # => 2014-04-01

$tp -= ONE_MONTH;
say $tp->mon; # => 3
say $tp->ymd; # => 2014-03-01

$tp -= ONE_MONTH;
say $tp->mon; # => 1 (!)
say $tp->ymd; # => 2014-01-30 (!!)

このコードは,Time::PieceのオブジェクトからONE_MONTHを引いてやる事によって1ヶ月前の月を表現したいという意図を表していますが,上手く動きません.
それもそのはず,Time::Secondsの実装は以下のようになっており,

https://github.com/rjbs/Time-Piece/blob/master/Seconds.pm#L29-L31

ONE_MONTHは2629744秒 (つまりONE_YEAR / 12) のようにして定義されています.
30日を秒に表すと60 * 60 * 24 * 30 = 2592000秒だからONE_MONTHよりも小さいわけですね.
加えて2014年2月は28日しか無く,つまり60 * 60 * 24 * 28 = 2419200秒なので,ここで決定的に差が生まれてしまったという訳ですね.これは具合が良くない! もちろん「1ヶ月前の今日」みたいなものも上手くは取れないですね.

そしてONE_YEARONE_YEARで31556930秒,つまり365.24225日として表現されている為,こちらも普通に使うとおかしな事になってしまいます.

use Time::Piece;
use Time::Seconds;

my $tp = localtime->strptime("2014", "%Y");

$tp -= ONE_YEAR;
say $tp->year; # 2012 (!)
say $tp->ymd;  # 2012-12-31 (!!)

のっけからやってくれる!!!! まあそれはそうですよね,という感じ.
こちらも「1年前の今日」みたいなものは上手く取れない.


で,どうするかというと,Time::Piece::Monthを使うと言った方法が考えられますが,このモジュールは内部でDate::Simpleを使っていて,Date::Simpleは呪われている(呪われていた)という問題があるので少し渋い.この方法でも問題なさそうだったらこれで良いとは思います.
問題が複雑では無かったら,LEAP_YEARNON_LEAP_YEARONE_FINANCIAL_MONTHや,あるいはONE_REAL_MONTHONE_REAL_YEARを駆使して自分で組み立てるというのも1つの手かもわかりません.

とにかく,ONE_MONTHONE_YEARを使って何らかの日付け操作をしているコードを見つけた時は疑ってみたほうが良いと思います.


手が空いたらここらへんをケアするシンプルなモジュールを書くのもやぶさかではありませんが,果たして手が空くのか!


[追記]
Time::Piece::Plus使えばええんや!!!!! という僕の中の僕が囁きましたのでシェアします.

[追記]
id:karupanerura氏「add_months/add_years ってメソッド使えや!!!」
とのこと.なんで僕はこのメソッドの存在を知らなかったのだろうか!?

ともかくONE_MONTHとかONE_YEARとかつかってたら大体おかしいことには違いないので,そういう場合は疑うってことでひとつ.