Upgrade to Pro — share decks privately, control downloads, hide ads and more …

perlimportsから探るPPIの世界

AnaTofuZ
March 05, 2022

 perlimportsから探るPPIの世界

YAPC::Japan::Online 2022の登壇資料です
https://yapcjapan.org/2022online/

AnaTofuZ

March 05, 2022
Tweet

More Decks by AnaTofuZ

Other Decks in Technology

Transcript

  1. perlimportsから探るPPIの世界
    YAPC::Japan::Online2022 03/05
    八雲アナグラ(AnaTofuZ)

    View Slide

  2. 自己紹介
    八雲アナグラ(@AnaTofuZ)
    Okinawa.pm -> ???
    最近は京都にいます
    株式会社はてなノベルチーム

    View Slide


  3. 今日のトークについて
    途中でピザが来ます
    来たら受け取りに行きます

    View Slide

  4. 今日の内容
    perlimportsの雰囲気とPPIの雰囲気がわかる

    View Slide

  5. 今日の内容
    perlimportsとは
    perlimportsの紹介
    perlのuse問題
    使い方
    perlimportsとPPI
    静的解析とは
    PPIの紹介
    perlimportsの処理の一部分を眺める

    View Slide

  6. perlimportsとは
    The Perl and Raku Conference (In the Cloud) 2021で発表されたツール
    https://metacpan.org/dist/App-perlimports
    作者はOALDERS(Olaf Alders)さん
    最近静的解析ツールにハマっているっぽい
    App::perlimports
    今日の内容
    App::perlvars
    コード上で未使用の変数検出ツール

    View Slide

  7. perlimportsとは
    perlのuse(モジュールロード)をいい感じにするのを助けてくれるツール
    具体的にはどの関数がどのモジュールから持ってきたかを明示的に作る規約のレー
    ルになるもの
    ソースコードの情報を解析するのに静的解析を使用している
    goimportsインスパイア
    Perlで実装されている

    View Slide

  8. 静的解析
    静的(コード実行することなしに)、コード情報を解析する手法
    コードに問題がないかを検出するlinterや、見た目をいい感じに整形するformatterな
    どはおおよそ何かしらの形で静的解析をしている
    逆にコード実行してコード情報を解析する商法に「動的解析」がある
    マルチスレッド環境におけるバグの発見などは動的解析のほうが望ましい
    Perlにおいても動的解析を使った課題解決はよく利用される
    https://techblog.cartaholdings.co.jp/entry/2020/05/07/120000
    Perlにおける静的解析の現状はこの後お話します

    View Slide

  9. perlimportsが解決したい問題

    View Slide

  10. Q. encode_json
    はどこから来ているでしょう
    use strict;

    use warnings;

    use JSON;

    print encode_json({hoge => 'hello', isBool => \0});

    View Slide

  11. Q. encode_json
    はどこから来ているでしょう
    A. 標準関数?
    use strict;

    use warnings;

    use JSON;

    print encode_json({hoge => 'hello', isBool => \0});

    View Slide

  12. Q. encode_json
    はどこから来ているでしょう
    A. 標準関数?
    use strict;

    use warnings;

    use JSON;

    print encode_json({hoge => 'hello', isBool => \0});

    View Slide

  13. Q. encode_json
    はどこから来ているでしょう
    A. 標準関数?
    A. JSON
    がエクスポートしている
    use strict;

    use warnings;

    use JSON;

    print encode_json({hoge => 'hello', isBool => \0});

    View Slide

  14. Perlのモジュールロードの方法
    大きく分けて2種類
    require
    use
    それぞれ次の違いがある
    ロード方法 ロードタイミング 副作用
    require require文実行時(逐次) ない
    use コンパイルタイム importを実行する

    View Slide

  15. ロードタイミングの比較
    require
    は文実行時
    use
    はコンパイルタイム
    具体的にどういう挙動の差を見せるか確認する
    存在しない Hoge
    モジュールをロードする例題を考える

    View Slide

  16. ロードタイミングの比較
    require
    の場合
    #!/usr/bin/env perl

    use strict;

    use warnings;

    print "before require\n";

    require Hoge;

    print "after require\n";

    View Slide

  17. ロードタイミングの比較
    require
    の場合
    #!/usr/bin/env perl

    use strict;

    use warnings;

    print "before require\n";

    require Hoge;

    print "after require\n";

    require
    の直前のprint文が実行されている
    逐次的に処理される
    ❯ perl hoge.pl

    before require

    Can't locate Hoge.pm in @INC (you may need to install the Hoge module)

    (@INC contains: ...) at hoge.pl line 8.

    View Slide

  18. ロードタイミングの比較
    use
    の場合
    #!/usr/bin/env perl

    use strict;

    use warnings;

    print "before use\n";

    use Hoge;

    print "after use\n";

    View Slide

  19. ロードタイミングの比較
    use
    の場合
    #!/usr/bin/env perl

    use strict;

    use warnings;

    print "before use\n";

    use Hoge;

    print "after use\n";

    何もprintされずに終了する
    スクリプトの実行前にuseが処理される
    ❯ perl hoge.pl

    Can't locate Hoge.pm in @INC (you may need to install the Hoge module)

    (@INC contains: ... ) at hoge.pl line 8.

    BEGIN failed--compilation aborted at hoge.pl line 8.

    View Slide

  20. useの実態
    実際は次の構文と等価
    use Hoge;

    BEGIN {

    require Hoge;

    Hoge->import();

    }

    BEGINブロック
    Perlでコンパイルタイム(最初)に実行したい処理を書けるブロック
    BEGINブロック内で require
    して import
    を実行する

    View Slide

  21. import
    Perlの規約的にuseしたタイミングで実行される関数
    何を書くかはオブジェクトを書く人に委ねられている
    よく use
    した先で通常の関数の様に使いたいモジュール定義の関数をエクスポート(輸
    出)する処理が書かれている
    エクスポートしない場合はJSONの例では JSON->encode_json
    の様に扱わなけれ
    ばならない
    この煩わしさを解消したいケースなどで関数エクスポートが使われる
    エクスポート以外にも使うことができる

    View Slide

  22. エクスポート以外のimport
    Class::Accessor::Liteではオブジェクトのアクセサなどの定義にimportを活用している
    use時に ()
    の中身に文字列以外を渡しているケースはだいたい独自にimportを書いてい

    use Class::Accessor::Lite (

    new => 1,

    ro => [ qw(baz) ],

    wo => [ qw(hoge) ],

    );

    sub import { #
    一部
    shift;

    my %args = @_;

    my $pkg = caller(0);

    my %key_ctor = (

    rw => \&_mk_accessors,

    ro => \&_mk_ro_accessors,

    ...

    View Slide

  23. 関数エクスポートをするimport
    様々なやり方があるが、最近はExporterを使うのが主流
    Exporterモジュールを継承、もしくはuseすると自動でエクスポート処理を import
    とし
    て登録する
    package Hoge::First;

    use strict;

    use warnings;

    use feature qw/say/;

    use Exporter 'import';

    our @EXPORT = (qw/hoge/);
     #
    サブルーチンの名前を文字列で配列の中にいれるとEXPORT
    される
    sub hoge {

    say 'this is first!';

    }

    1;

    View Slide

  24. 関数エクスポートをするimport
    @EXPORT
    に登録されているシンボル(関数名)をエクスポートする
    このコードでは hoge
    がエクスポートされる
    package Hoge::First;

    use strict;

    use warnings;

    use feature qw/say/;

    use Exporter 'import';

    our @EXPORT = (qw/hoge/);
     #
    サブルーチンの名前を文字列で配列の中にいれるとEXPORT
    される
    sub hoge {

    say 'this is first!';

    }

    1;

    View Slide

  25. 関数エクスポートしたものをuseする
    hoge
    メソッドをHoge::First, Hoge::Secondでそれぞれ定義、エクスポートする
    #!/usr/bin/env perl

    use strict;

    use warnings;

    use Hoge::First;

    use Hoge::Second;

    hoge();

    View Slide

  26. 関数エクスポートしたものをuseする
    hoge
    メソッドをHoge::First, Hoge::Secondでそれぞれ定義、エクスポートする
    この場合呼び出される hoge
    は最後にロードした Hoge::Second
    のものになる
    #!/usr/bin/env perl

    use strict;

    use warnings;

    use Hoge::First;

    use Hoge::Second;

    hoge();

    ❯ perl -Ilib hoge.pl

    this is second!

    View Slide

  27. 関数エクスポートが起こしがちな問題
    先程の例の様に、同名の関数をエクスポートしていた場合、ロード順によって呼び出さ
    れる関数の実態が変わってしまう
    そもそもどこで定義している関数なのか探しにくい

    View Slide

  28. 関数エクスポートが起こしがちな問題
    先程の例の様に、同名の関数をエクスポートしていた場合、ロード順によって呼び出さ
    れる関数の実態が変わってしまう
    そもそもどこで定義している関数なのか探しにくい
    → 何をどこから持ってきたのか明示的に書けばわかりやすい!

    View Slide

  29. EXPORT_OK
    @EXPORT
    に詰めたシンボルは無条件でエクスポートされてしまう
    対して @EXPORT_OK
    はデフォルトではすべてをロードせず、use時にインポートしたい
    関数名を書くことを強制させられる
    use JSON qw(encode_json); # encode_json
    がインポートされる

    @EXPORT
    の場合も関数名を use
    モジュール名 の後ろに列挙すると指定したもののみイ
    ンポートできる
    空を指定すると何もインポートしない
    use JSON qw(); # encode_json
    はインポートされないので使えない

    View Slide

  30. EXPORTのまとめ
    Perlはモジュールを use
    する時に、モジュールにimportがあれば実行する
    importには関数エクスポートがしばしば定義されていて、関数をインポートすると、モ
    ジュール側で定義した関数を呼び出したスクリプトファイル内で定義した様に使える
    しばしばどこで関数を定義したかわからなくなるので、 use
    の際にインポートしたい関
    数名だけ列挙することができる

    View Slide

  31. perlimportsが解決したい課題
    どの関数がどのモジュールによってエクスポートされてるかを明示的に書きたい!!
    これをシステマティックにやってほしい!!

    View Slide

  32. perlimportsとは
    perlのuse(モジュールロード)をいい感じにするのを助けてくれるツール
    CLIツールなので編集したいPerlファイルにたいして実行する
    実行するとuseにコード中に使用している関数に対応する qw()
    指定を自動でつけてくれ

    View Slide

  33. 混沌としたコード
    use MyPkg::One;

    use MyPkg::Two;

    use MyPkg::Three;

    use MyPkg::Four;

    f1();

    f2();

    MyPkg::Three->f3();

    f1はMyPkg::OneがEXPORTしているものを使っている
    f2はMyPkg::TwoがEXPORT_OKしているが、コード上は指定を忘れている→

    View Slide

  34. useしているモジュールの実装
    package MyPkg::Two;

    use strict;

    use warnings;

    use Exporter 'import';

    our @EXPORT_OK = qw(f2);

    sub f2 {

    print "f2\n";

    }

    1;

    f2はMyPkg::TwoがEXPORT_OKで宣言している
    useするものを書かないとf2がインポートされない
    先程のコードではインポートしきれていないのでf2の実行で処理が止まってしまう

    View Slide

  35. perlimportsを使えば混沌としたコードも
    use MyPkg::One;

    use MyPkg::Two;

    use MyPkg::Three;

    use MyPkg::Four;

    f1();

    f2();

    MyPkg::Three->f3();

    f1はMyPkg::OneがEXPORTしているものを使っている
    f2はMyPkg::TwoがEXPORT_OKしているが、コード上は指定を忘れている
    f3はフルパスで呼び出している
    MyPkg::Four
    はuseしているが特に使っていない

    View Slide

  36. perlimportsを使えば混沌としたコードも
    $ perlimports --libs lib --no-preserve-unused main.pl

    use MyPkg::One qw( f1 );

    use MyPkg::Two qw( f2 );

    use MyPkg::Three ();

    f1();

    f2();

    MyPkg::Three->f3();

    f1
    , f2
    が適切にインポートされた
    特に関数をインポートしない MyPkg::Three
    は ()
    を指定するようになった
    使用していない MyPkg::Four
    は use
    が消えた

    View Slide

  37. perlimportsのツラミポイント
    Class::Accessor::Liteなど、 import
    を関数エクスポート以外に使っているケースのもの
    は、明示的に無視するようにperlimportsに教える必要がある
    関数エクスポートしていないとみなされて ()
    になってしまうケースが有る
    最近修正されつつあるので発生しないものもある
    use Class::Accessor::Lite (

    rw => [qw/ hoge/],

    );

    use Class::Accessor::Lite ();

    View Slide

  38. 特定のモジュールを無視する方法
    色々な方法で無視することが可能だが、最近出た正規表現で無視するモジュール名を指
    定する方法が便利
    無視するモジュール名の規則を書いたファイルをperlimportsにわたす
    $ perlimports --ignore-modules-pattern-filename ignore.txt hoge.pl

    ^Class

    ^Acme::AnaTofuZ

    View Slide

  39. perlimportsの思想
    基本的にすべての関数エクスポートは明示的に行いたい
    関数エクスポートしないuseであるならば、絶対にエクスポートにしないように強
    制してくる
    use Acme::AnaTofuZ;

    これだとAcme::AnaTofuZ側が @EXPORT
    で宣言されていた場合何かをエクスポートして
    くる可能性がある
    ==> こんな感じに書き換えてくる
    use Acme::AnaTofuZ ();

    やろうとしてることはわかるけどなんか見た目がアレ...

    View Slide

  40. perlimportsまとめ
    いい感じにuseするのを助けてくれるツール
    どこから来たかわからない関数を実行しなくて良いので使うのがおすすめ
    若干思想が強い癖はある
    静的解析をして使用しているモジュールを特定している
    静的解析について見ていくぞ!!!

    View Slide

  41. 静的解析(おさらい)
    静的(コード実行することなしに)、コード情報を解析する手法
    コードに問題がないかを検出するlinterや、見た目をいい感じに整形するformatterな
    どはおおよそ何かしらの形で静的解析をしている
    逆にコード実行してコード情報を解析する商法に「動的解析」がある
    マルチスレッド環境におけるバグの発見などは動的解析のほうが望ましい

    View Slide

  42. 静的解析とPerl
    Perlの静的解析はめちゃくちゃ難しいとされている
    Nothing but perl can parse Perl
    Perlのコードはperl(処理系)以外はパースできない
    静的解析するツールを作成するのが極めて困難
    perl処理系が何かしらの静的解析で使いやすいAPIを提供している訳でもない

    View Slide

  43. ちなみに何でPerlの解析が難しいのか
    PPIのドキュメントにある例を見ると以下の状況
    @result = (dothis $foo, $bar);

    Perlは関数呼び出しの ()
    を省略できるので、 dothis
    は何かしらの関数呼び出しである
    しかしこの記述だと次のどちらかに相当するのかパット見で判断つかない
    @result = (dothis($foo), $bar);

    @result = dothis($foo, $bar);

    View Slide

  44. なぜ判断つかないのか
    @result = (dothis($foo), $bar);

    @result = dothis($foo, $bar);

    通常は関数呼び出しは後ろの引数をすべて取る
    Perlは関数プロトタイプ機能があり、引数を1つだけに成約することができる
    BEGIN
    ブロック内で何かしらの成約が追加されている可能性がある
    例えば特定の状況で dothis
    の実態を切り替えるなど
    はたまたまだ dothis
    が宣言されていない可能性がある
    ...むずい!!!

    View Slide

  45. 静的解析とPerl
    Perlの静的解析はめちゃくちゃ難しいとされている
    Nothing but perl can parse Perl
    Perlのコードはperl(処理系)以外はパースできない
    静的解析するツールを作成するのが極めて困難
    perl処理系が何かしらの静的解析で使いやすいAPIを提供している訳でもない
    それでも先人がツールを作成しているので巨人の肩に乗るのが良い
    今回はperlimportsが使っているPPIを見ていきます
    他にもPerlの静的解析ツールは色々生み出されています
    ライブラリの特性などを見る場合はYAPC::Tokyoの発表がおすすめです
    Perl5の静的解析入門

    View Slide

  46. PPI
    Perlで書かれたperlインタプリタの助けなしに動作するPerlの静的解析ツール
    2022/02/27現在の最新バージョンは1.272
    ソースコードはGitHubのPerl::Criticチームが持っている
    https://github.com/Perl-Critic/PPI

    View Slide

  47. PPIの特徴
    Round Trip Safe(往復安全)
    PPI <-> Perlの変換を何回繰り返しても同じ結果になる
    空白やコメントも含めてPPIがPerlスクリプトを解析した結果を保持する
    大体のPerlプログラムを解析できる
    At time of writing there are only 28 non-Acme Perl modules in CPAN that PPI is
    incapable of parsing.
    解析出来なかったものはそもそもぶっ壊れているモジュールだったとのこと

    View Slide

  48. PPIの構成
    大きくPPI::TokenizerとPPI::Lexerで構成されている
    解析した結果はPerl Document Object Model(PDOM)として返る
    PDOM
    ソースコードを表現した木構造のPerlのオブジェクト
    いわゆる抽象構文木に相当するもの
    Perlコードに対応する要素は PPI::Element
    として表現され、PDOMはその集合を持つ
    PDOMはあくまでPPIが作った解析用のオブジェクトモデル
    perl処理系のオブジェクトモデルとは異なっている
    PPIを活用する場合基本はこのPDOMを取り回すことになる

    View Slide

  49. PDOMを眺める
    PPIにはPDOMを出力できるDumperが付随している
    my $hoge = "hello";

    ソースコードの左側に表示されているのが対応する PPI::Element
    まとまりがインデントで表現されていると捉えればわかりやすい
    PPI::Document

    PPI::Statement::Variable

    PPI::Token::Word 'my'

    PPI::Token::Whitespace ' '

    PPI::Token::Symbol '$hoge'

    PPI::Token::Whitespace ' '

    PPI::Token::Operator '='

    PPI::Token::Whitespace ' '

    PPI::Token::Quote::Double '"hello"'

    PPI::Token::Structure ';'

    PPI::Token::Whitespace '\n'

    View Slide

  50. PPI::Element
    Perlのコードに対応するPPI上の表現
    なんかいいかんじにコード情報にアクセスするインターフェイスが整っている
    例えば "hoge"
    の様なダブルクォーテーションで囲われたなにかに対応する
    PPI::Token::Quote::Double
    がある

    View Slide

  51. 例えばPerlコード上の "hoge"
    に対応するElementを見てみる
    PPI::Token::Quote::Double->string
    で中身を感じに文字列化することができ

    my $quote_tokens = $doc->find(

    sub { $_[1]->isa('PPI::Token::Quote::Double') }

    );

    my $quote = $quote_tokens->[0];

    say $quote->string; # hello

    PerlコードとPPI::Elementの対応がわかれば、 PPIのインターフェイスを通じてPerlのコ
    ード情報をPerlで操作することができる
    難しそうな静的解析も普段のPerlプログラミングのレイヤーに

    View Slide

  52. useに相当するPPI::Element
    perlimports
    は use
    をいい感じにするモジュール
    use
    がPPI::Elementでどう表現されるかを把握しておくと何をやっているかがわか
    りやすい
    PPI::Statement::Include
    があり、以下の例はすべて Statement::Include
    として
    解釈される
    use 5.006;

    use strict;

    use My::Module;

    use constant FOO => 'Foo';

    require Foo::Bar;

    require "Foo/Bar.pm";

    require $foo if 1;

    no strict 'refs';

    View Slide

  53. use JSON;をPPI::Elementに
    例えば次の特に何もインポートするシンボルを指定していないuseをPPI::Elementに変換
    してみる
    use JSON;

    1つめの PPI::Token::Word
    に use
    が格納されている
    2つめの PPI::Token::Word
    にモジュール名が格納されている
    PPI::Document

    PPI::Statement::Include
    PPI::Token::Word 'use'

    PPI::Token::Whitespace ' '

    PPI::Token::Word 'JSON'

    PPI::Token::Structure ';'

    PPI::Token::Whitespace '\n'

    View Slide

  54. use JSON qw(encode_json);をPPI::Elementに
    encode_json
    を指定した状態でPPI::Elementに変換する
    use JSON qw(encode_json);

    2つめのWordの後ろに QuoteLike::Words
    がある
    ここにインポートしたい関数の名前がある
    PPI::Document

    PPI::Statement::Include
    PPI::Token::Word 'use'

    PPI::Token::Whitespace ' '

    PPI::Token::Word 'JSON'

    PPI::Token::Whitespace ' '

    PPI::Token::QuoteLike::Words 'qw(encode_json)'

    PPI::Token::Structure ';'

    PPI::Token::Whitespace '\n'

    View Slide

  55. use JSON qw(encode_json);をPPI::Elementに
    何もインポートしない ()
    を指定した状態でPPI::Elementに変換する
    use JSON ();

    2つめのWordの後ろに Structure::List
    がある
    今回のケースではその中身は空文字
    PPI::Document

    PPI::Statement::Include
    PPI::Token::Word 'use'

    PPI::Token::Whitespace ' '

    PPI::Token::Word 'JSON'

    PPI::Token::Whitespace ' '

    PPI::Structure::List ( ... )

    PPI::Token::Structure ';'

    PPI::Token::Whitespace '\n'

    View Slide

  56. PPI::Statement::Includeからたぐるモジュールuseの構成
    以下の状況を満たすPDOMは use
    しているものであると判断できる
    PPI::Statement::Includeのブロックであること
    最初の PPI::Token::Word
    が use
    であること
    次の PPI::Token::Word
    が呼び出すモジュール名であること
    指定がある場合で qw
    を使っていた場合は次に
    PPI::Token::QuoteLike::Words
    があり、なにか入っている
    素朴な ()
    の場合は PPI::Structure::List
    が来ており、空白の場合は中身
    がないこと
    ※ use Exporter 'import';
    など、 ()
    を使わないケースもあるので必ずしもこ
    の条件が全てではない

    View Slide

  57. perlimportsの処理を眺める
    実際に use
    を解析している箇所を眺めてみます
    findメソッドで指定した条件に一致するPDOMを取り出す事ができる
    これは PPI::Statement::Include
    でかつ use
    のものを指定している
    先程まで見ていたケースのPDOMを取り出す
    sub _build_original_imports {

    #...

    my $found = $self->ppi_document->find(

    sub {

    $_[1]->isa('PPI::Statement::Include')

    && !$_[1]->pragma # no pragmas
    && !$_[1]->version # Perl version requirement

    && $_[1]->type

    && $_[1]->type eq 'use';

    }

    ) || [];

    View Slide

  58. useを取り出してきた後は
    perlimportsは今現在どのモジュールが何をインポートしているかを把握する必要がある
    現状は、 モジュール名 => [
    インポートしているシンボル]
    のhashrefを作る
    入力されたコード
    use Carp;

    use Data::Dumper qw( Dumper );

    use POSIX ();

    perlimportsが作る内部データ構造
    {

    Carp => undef,

    'Data::Dumper' => ['Dumper'],

    POSIX => [],

    }

    View Slide

  59. useするモジュール名
    PPI::Statement::Documentのmoduleメソッドを使うと、useしているモジュール名が取
    得できる
    use Carp;
    であれば Carp
    が取得できる
    実際のperlimportsでモジュール名を取得している箇所
    for my $include ( @{$found} ) {

    my $pkg = $include->module;

    View Slide

  60. useする際に指定しているシンボル名
    PDOMでの表現を再度確認
    use JSON qw(encode_json);

    2つめのWordの後ろに QuoteLike::Words
    の中にシンボルがある
    Wordsの中を見ていけばよい
    PPI::Document

    PPI::Statement::Include
    PPI::Token::Word 'use'

    PPI::Token::Whitespace ' '

    PPI::Token::Word 'JSON'

    PPI::Token::Whitespace ' '

    PPI::Token::QuoteLike::Words 'qw(encode_json)'

    PPI::Token::Structure ';'

    PPI::Token::Whitespace '\n'

    View Slide

  61. useする際に指定しているシンボル名
    気合でループしてシンボルを探している
    よいですね
    for my $child ( $include->schildren ) {

    if ( $child->isa('PPI::Structure::List')

    && !defined $imports ) {

    $imports = [];

    }

    if ( !$child->isa('PPI::Token::QuoteLike::Words')

    && !$child->isa('PPI::Token::Quote::Single') ) {

    next;

    }

    my @imports = $child->literal;

    if ( defined $imports ) {

    push( @{$imports}, $child->literal );

    }

    else {

    $imports = [ $child->literal ];

    }

    }

    return $imports;

    View Slide

  62. 読める気になってきましたね?
    PPI::Documentとさえ仲良くなればいい感じにコードが読める...!!
    あとは気合

    View Slide

  63. まとめ
    use
    をいい感じにするperlimportsがある
    perlimportsは静的解析ツールのPPIを使っている
    PPIはむずそうだが、PPI::Elementと仲良くなれば普通のPerlコード
    PPIと仲良くなると最近のいいツールのコールドリーディングができる

    View Slide