Masteries

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

VSCodeで開かれたシェルであることを環境変数から知る方法

結論から述べると,

qiita.com

こちらのQiitaエントリの, 統合ターミナルかその他ターミナルかを判別するの項を読みましょう.


「VSCodeで開いたシェルだけ○○をする」といったことを実現しようと思った時, Googleなどで検索すると VSCODE_PID 環境変数で識別するとよい... という情報を得ることができたのですが, VSCode 1.26より VSCODE_PID 環境変数はセットされなくなったので, 上記エントリにもあるように, TERM_PROGRAM 環境変数で識別するようにしましょう.

# VSCodeで開かれたシェル
$ echo $TERM_PROGRAM
vscode

# 例: iTerm2で開かれたシェル
$ echo $TERM_PROGRAM
iTerm.app

...といった形で, どのプログラムでシェルを開いたか? という情報が TERM_PROGRAM 環境変数に入っているようです. これを知るまで数時間潰してしまったので, 備忘録のために書き記しておきます.

Syntax::Keyword::TryとPerlのキーワードプラグイン (その1)

id:papix です. この記事は, Perl Advent Calendar 2019の2日目の記事です. 昨日は, id:karupanerura さんの「2019年の最先端のPerl開発ボイラープレート」でした.

qiita.com

今日は, 昨日のエントリでも触れられていたSyntax::Keyword::Tryについて, さっくりとアレコレ綴りたいと思います.

Perlと例外処理

Perlで例外処理をするにあたって, 例えばJavaのようなtrycatchといった仕組みは言語仕様として提供されていません. そのため, Perl Monger達はtry/catchのような機能を提供するモジュールを用意して対処してきました. その中でも著名なものの1つは, Try::Tinyでしょう.

metacpan.org

Try::Tinyを使えば, 例外(die)が起こるコードについて, 次のように書くことができます.

use Try::Tiny;

sub run {
    try {
        die "!!!";
    } catch {
        warn "recovery";
    };
}

run();

tryで囲まれたコードで例外が発生した場合, Try::Tinyはそれをよしなにcatchしてくれて, catchで囲まれたコードを実行してくれる訳です.

Try::Tinyの罠

さて, Try::Tinyには, 何だかんだで誰もが1度はハマる罠が存在します. それが次のコードです.

use Try::Tiny;

sub run {
    try {
        # ... 例外が起こるかもしれない処理 ...
    } catch {
        warn "recovery";
        return 1;
    };

    # ... 何かしらの処理 ...

    return 2;
}

my $result = run();
print "$result\n";

このコードを実行したとき, print "$result\n"; は何と表示されるでしょうか? コードを書いた人の意図としては,

  • tryで囲まれたコードで例外が発生すれば, catch で囲まれたコードが実行され, 結果としてrunは1を返す
  • tryで囲まれたコードで例外が発生しなければ, # ... 何かしらの処理 ... の部分にある何かしらのロジックが実行され, 結果としてrunは2を返す

...と考えていそうです. しかしながら, 例えば # ... 例外が起こるかもしれない処理 ... を, 実際に例外が起こるようなコード(例えばdie("!!!"))にしたとしても, このコードの実行結果(= print "$result\n";の出力)は, 2になります.

これは何故でしょうか? 結論から言えば, Try::TinyはPerlの既存の仕組み(プロトタイプなど)を利用して, 強引にtry/catch風の構文を再現しているから... ということになります. 実は, 先のコードは下記のコードと等価なのです.

    try (
        sub {
            # ... 例外が起こるかもしれない処理 ...
        },
        catch (
            sub {
                warn "recovery";
                return 1;
            },
        ),
    );

そのため, return 1はTry::Tinyが提供するcatchという関数の第一引数に渡るサブルーチンリファレンスの返り値となり, 最終的にこの値はTry::Tinyが提供するtryという関数の返り値となるわけです.

また, Try::Tinyの場合, 最後のコードブロックに;をつける必要があるというのも, よくある罠と言えるでしょう.

use Try::Tiny;

sub run {
    try {
        # ... 例外が起こるかもしれない処理 ...
    } catch {
        warn "recovery";
        return 1;
    } # ← ここに ; が必要!!!

    # ... 何かしらの処理 ...

    return 2;
}

Syntax::Keyword::Try

さて, そういったTry::Tinyの問題を解決する, Syntax::Keyword::Tryというモジュールがあります.

metacpan.org

このモジュールを使うと, Perlにおける例外処理を次のように実装することができます:

use Syntax::Keyword::Try;

sub run {
    try {
        # ... 例外が起こるかもしれない処理 ...
    } catch {
        warn "recovery";
        return 1;
    }

    # ... 何かしらの処理 ...

    return 2;
}

my $result = run();
print "$result\n";

末尾の;が不要だったり, # ... 例外が起こるかもしれない処理 ...で例外が起おきた時も, 関数runは当初意図していた通り, 1を返します(当然, 例外が発生しなければ return 2 によって2を返す). Syntax::Keyword::Tryには, Try::Tinyに潜む罠が存在しないのです.

「キーワードプラグイン」

さて, このモジュールは, Perlの"キーワードプラグイン"という仕組みを使って実装されています. キーワードプラグインはPerl 5.12(2010年リリース)から実装された機能で, Perlのパーサーにフックを設定し, 構文を拡張できるようになっています.

Syntax::Keyword::Try以外にも, キーワードプラグインを利用したモジュールは公開されていて, 例えばFunction::Parameters(関数の定義をより良い形で記述できる), Keyword::Boolean(true/falseを提供する), Switch::Plain(switch構文を提供する)などがあります.

metacpan.org

metacpan.org

metacpan.org

id:charsbar さんが2016年に参加した, London Perl Workshop 2016で, Syntax::Keyword::Tryを開発した, Paul Evans氏の発表があったそうですが,

Perlをもっとよくしていくには、公開されている機能をギリギリまで使い倒して、できる人たちに「そんな無茶なやり方よりもっといい方法がある」と言わせるといい

charsbar.hatenadiary.org

...と仰っていたそうです. キーワードプラグインによって今のPerlを拡張し, 日々の開発に役立て, それを通して未来のPerlに貢献する... というのは, とてもおもしろそうですね.

次回予告

...というわけで, キーワードプラグインの実装までご紹介出来ればよかったのですが, ちょっと準備に時間が足りず... 今回はここまでとさせてください. 改めて, Perl Advent Calendar 2019の16日目にて, 「キーワードプラグインを使ったモジュールの実装について」をご紹介させてもらえればと思います!!!!!!

Perl Advent Calendar 2019, 明日の担当は, take_3さんです.

詳解 Class::Accessor::Typed

Kichijoji.pm20で発表した, 「詳解 Class::Accessor::Typed」の資料を公開します. 今回は実験的にScrapboxに書いてプレゼンテーションモードで発表したので, それを加筆修正してはてなブログで公開することとしました.

Perlの「型」について

  • そもそも型とは?
    • Wikipedia曰く...
    • コンピュータで扱うデータの決められた形式・種類(整数型、浮動小数点型、文字型、集合型、ポインタ型 等)→データ型
  • Perlは動的型付け言語
    • GoやScalaのような静的な型はない
    • perldocのperldataによると...
      • Perl には、スカラ、スカラの配列、「ハッシュ」とも呼ばれるスカラの 連想配列という 3 つの組み込みデータ型があります。
    • ナルホド...

型制約

  • 例えば...
sub hello {
    my $name = shift;
    printf "Hello, %s!!!", $name;
}

hello('papix'); # => Hello, papix!!!
hello(+{}); # => Hello, HASH(0x7fdabc8035e0)!!!
  • Perlのスカラ変数は(当然)様々な種類のデータを持てる
    • 文字列のようなもの, 数字のようなもの, 配列やハッシュのリファレンス...
    • 例えば, 文字列を期待する所にリファレンスが来ると困る
      • エラーになる, 警告が出る...
      • 何れにせよ, 期待通りの結果にならない!!
        • 「期待通りの結果にならない」のに, 正しく動作するように見えるとかなり辛い
        • 例えば, 上のサンプルコードはHASH(0x7fdabc8035e0)という意図しない出力をするが, エラーにはならず処理はそのまま実行され続ける(警告は出る)
  • 静的型付け言語であれば, コンパイル時にエラーとすることができる
    • しかし, 動的型付け言語は実行時に型が動的に定まるので, コンパイル時にエラーにすることができない...

ソリューション

  • モジュールで動的に型を定義するアプローチ
    • 前述のように, Perlは動的型付け言語 = 実行するまで何が来るかわからない
      • モジュールで型を定義し, 実行時にそれを使って検証する
        • 型が適切でない場合, エラーになる
        • しかし, エラーにならず, そのまま素通りしてしまうよりは遥かにマシ
    • 例: Smart::Args
      • hello(+{}); は, Smart::Argsによってエラーとなる(その時点でdieする)
use Smart::Args qw(args_pos);

sub hello {
    args_pos my $name => 'Str';
    printf "Hello, %s!!!", $name;
}

hello('papix'); # => Hello, papix!!!
hello(+{}); # => '$name': Validation failed for 'Str' with value HASH(0x7f8f298035e0)
  • 型の定義
    • 例: Mouse::Util::TypeConstraints
      • クラスビルダーのMouseに付属している, 基本的な型 + 型の拡張を実現するモジュール
        • Smart::Argsも内部ではこれを使っている
        • Type::Tinyというモジュールを使ったSmart::Args::Type::Tinyというモジュールもある
      • 次のような型が用意されている:
 Any
  Item
      Bool
      Maybe[]
      Undef
      Defined
          Value
              Str
                  Num
                      Int
                  ClassName
                  RoleName
          Ref
              ScalarRef
              ArrayRef[]
              HashRef[]
              CodeRef
              RegexpRef
              GlobRef
                  FileHandle
              Object

Perlのクラスビルダーについて

クラスビルダーとは...

  • Perlには, もともとオブジェクト指向プログラミングという概念はなかった
    • 後に, blessなどの仕組みを作って, 「後付で」実装された
package MyPackage;

sub new {
    my ($class, $name) = @_;
    return bless { # blessを使って, hash referenceをMyPackageで"祝福"する
        name => $name,
    }, $class;
}

sub name {
    my $self = shift;
    return $self->{name};
}
use MyPackage;

my $obj = MyPackage->new(name => 'papix');
print $obj->name; # => papix
  • このあたりの記述を, より便利に/シンプルに記述できるようにするのがPerlにおける「クラスビルダー」
  • Perl界では, さっくり大別して, 「フルスペック」なクラスビルダーと「シンプル」なクラスビルダーがある
    • フルスペック
      • getter/setter(アクセサ)などが自動的に提供される
      • 型によるバリデーションなどがある
      • Mixinなどを実現するための仕組みがある
    • シンプル
      • getter/setter(アクセサ)の提供をするくらい

フルスペック

  • Mouse
    • XSを活用しまくっていて軽量
      • 国内ではデファクトスタンダード(と思う)
    • 注: XSというのは, Perlの処理をC言語で実装するための仕組みです
      • C言語で実装されているのでPerlに比べて高速
  • Moose / Moo
    • 最近はMooを使うよう推奨されている
    • Moose / Mooをよしなに使い分ける, Any::Mooseというモジュールもあった
  • Mo, Mとかもある... (ネタモジュールの域)

シンプル

  • Class::Accessor / Class::Accessor::Fast
  • Class::Accessor::Lite
    • Class::Accessorを更にシンプルにした実装
  • Class::Accessor::Lite::Lazy
    • Class::Accessor::Liteに遅延ロードを提供するモジュール

Class::Accessor::Lite

  • newが真ならコンストラクタを自動生成する
  • rw/ro/woで, それぞれread/write, read only, write onlyのアクセサを生成する
package MyPackage;

use Class::Accessor::Lite (
    new => 1,
    rw  => [ qw(foo) ],
    ro  => [ qw(bar) ],
);
use MyPackage;

my $obj = MyPackage->new(
    foo => 'a',
    bar => 'b',
);

print $obj->foo; # => a
$obj->foo(123);
print $obj->foo; # => 123

print $obj->bar; # => b
$obj->bar(123); # 'main' cannot access the value of 'bar' on objects of class 'MyPackage'

Class::Accessor::Lite::Lazy

  • Class::Accessor::Liteに加えて, rw_lazy, ro_lazyを提供
    • コンストラクタに値を渡さなかった時, デフォルト値を遅延して生成できる(アクセサを読みに行った時に生成する)
  • 例えば次のコードは, fooを読み込んだ時に_build_fooの返り値が自動的に初期値として使われる
package MyPackage;

use Class::Accessor::Lite::Lazy (
    rw_lazy => {
        foo => '_build_foo',
    }
);
sub _build_foo {
    my $self = shift;
    ...
}

課題感

  • フルスペックのクラスビルダーは重厚長大
    • コードも複雑
    • しかし型の恩恵を得られるのは利点
  • シンプルなクラスビルダーはコードも単純
    • しかし型の恩恵はない

Class::Accessor::Typedの紹介

  • シンプルなクラスビルダー(Class::Accessor::Lite)を軸に, 型の恩恵を受けられるようにした
    • Smart::Args like (Mouse::Util::TypeConstraintsベース)
    • Class::Accessor::Lite / Class::Accessor::Lite::Lazy + Smart::Args, みたいな雰囲気
      • はてなではよく使われる組み合わせ
  • シンプルなクラスビルダーのようにコードは(フルスペックのクラスビルダーと比べると)かなり単純で, しかし型の恩恵を受けることができる

使い方

Class::Accessor::LiteやClass::Accessor::Lite::Lazyを使ったことがある方であれば, 使い方がかなり似ているということがわかるのではないかと思います.

package MyPackage;

use Class::Accessor::Typed (
    rw => {
        baz => { isa => 'Str', default => 'string' },
    },
    ro => {
        foo => 'Str',
        bar => 'Int',
    },
    wo => {
        hoge => 'Int',
    },
    rw_lazy => {
        foo_lazy => 'Str',
    }
    ro_lazy => {
        bar_lazy => { isa => 'Int', builder => 'bar_lazy_builder' },
    }
    new => 1,
);

sub _build_foo_lazy  { 'string' }
sub bar_lazy_builder { 'string' }

以下, Class::Accessor::Typedでできる事をコードを例示しつつ説明します.

型のチェック

コンストラクタに渡した値が指定した型を満たさない場合, エラーになります.

{
    package MyPackage;
    use Class::Accessor::Typed (
        rw => {
            foo => 'Int',
        },
        new => 1,
    );
}

my $obj = MyPackage->new( foo => 'string' ); # => 'foo': Validation failed for 'Int' with value string

もちろん, setterで値を上書きした時も型によるチェックが行われます.

{
    package MyPackage;
    use Class::Accessor::Typed (
        rw => {
            foo => 'Int',
        },
        new => 1,
    );
}

my $obj = MyPackage->new( foo => 123 );
$obj->foo('string'); # => 'foo': Validation failed for 'Int' with value string

コンストラクタ

コンストラクタの引数が足りない場合, エラーになります.

{
    package MyPackage;
    use Class::Accessor::Typed (
        rw => {
            foo => 'Int',
        },
        new => 1,
    );
}

my $obj = MyPackage->new(); # => missing mandatory parameter named '$foo'

ちなみに存在しないパラメータを渡した時は警告が出ます.

{
    package MyPackage;
    use Class::Accessor::Typed (
        rw => {
            foo => 'Int',
        },
        new => 1,
    );
}

my $obj = MyPackage->new(foo => 1, bar => 1); # unknown arguments: bar

コンストラクタの引数が足りない時, もしdefaultオプションが指定されていれば, それを利用します.

{
    package MyPackage;
    use Class::Accessor::Typed (
        rw => {
            foo => { isa => 'Int', default => 123 },
        },
        new => 1,
    );
}

my $obj = MyPackage->new();
print $obj->foo; # => 123

また, rw_lazy, ro_lazy を利用すれば, アクセサから読み込んだ時に値を生成できます.

{
    package MyPackage;
    use Class::Accessor::Typed (
        rw_lazy => {
            foo => 'Int',
        },
        new => 1,
    );

    sub _build_foo { 123 }
}

my $obj = MyPackage->new();
print $obj->foo; # => 123

ベンチマーク

                Rate Moose Moo(ISA) C::A::Typed  Moo C::A::Fast Mouse C::A::Lite
Moose        25830/s    --     -89%        -92% -96%       -96%  -97%       -97%
Moo(ISA)    240604/s  831%       --        -27% -60%       -62%  -70%       -76%
C::A::Typed 328975/s 1174%      37%          -- -45%       -49%  -59%       -67%
Moo         595781/s 2207%     148%         81%   --        -7%  -25%       -39%
C::A::Fast  641222/s 2382%     167%         95%   8%         --  -20%       -35%
Mouse       799455/s 2995%     232%        143%  34%        25%    --       -19%
C::A::Lite  983039/s 3706%     309%        199%  65%        53%   23%         --
  • Mooseや, 型のチェックをするMoo(Moo(ISA))に比べると, Class::Accessor::Typedは高速
  • 流石に, Class::Accessor::Fast, Class::Accessor::Liteよりは遅い
  • 驚異的なのはMouseで, なんとフルスタックなクラスビルダーなのにClass::Accessor::Typedより普通に高速(!?)
    • 流石XS...!!!

展望

  • Anikiのinflateのような仕組みを入れたい
    • 初期値を渡した時, 指定したサブルーチンを通して格納する
    • 例えば, 日付の文字列を渡すと, サブルーチンを通してDateTimeなどに自動的に変換するとか...
  • 以下のコードではfilterというオプションにしているけれど, どういう名前が良いか...?
    • ご意見募集中です...
package MyPackage;
use Class::Accessor::Typed (
    rw => {
        name => { isa => 'Str', filter => sub { $_[0] . "!!!" } },
    },
);
use MyPackage;

my $obj = MyPackage->new(name => 'papix');
print $obj->name; # => papix!!!

※Kichijoji.pm20終了後, ひとまず実装してみました:

github.com

まとめ

Class::Accessor::Typedを作ってみました. フルスタックではないけれど, そこそこ可読性が高く便利で, かゆい所に手が届くモジュールになったのではないかと思っています. 興味がある方は是非使ってみて下さい. また, ご意見ご感想やPull Requestも常時募集中です.

Class::Accessor::Typed 0.02をリリースしました

Class::Accessor::Typedをリリースしました - Masteries

こちらで紹介した, Class::Accessor::Typedの0.02を先程リリースしました.

metacpan.org

今回の変更点としては, id:motemen さん謹製のClass::Accessor::Lite::Lazyのようなrw_lazy/ro_lazyを追加しました. これで, オブジェクト生成時に初期値を与えなかったプロパティについて, 呼び出し時にデフォルト値を生成する... ということが可能になります.

ひとまず最低限欲しかった機能はこれで実装しきりましたが, (実行速度が遅くなりすぎない範囲で)便利な機能はちまちまと足していきたいと思っています. 要望やバグレポートなどあれば, GitHubのIssueなどで投げてもらえると助かります!!!! もちろんpatches welcomeです.

github.com

Class::Accessor::Typedをリリースしました

papix.hatenablog.com

こちらのエントリで紹介したClass::Accessor::Type改めClass::Accessor::TypedをCPANにリリースしました.

metacpan.org

Class::Accessor::Liteを便利に使っているが, もう少しSmart::Argsのように型の恩恵を受けたい... という時に使ってみて下さい. 今後の展望としては,

  • コードをもう少し見直して, 高速化出来ないかの検討
  • Class::Accessor::Lite::Lazyのように, 後からdefault値を評価出来るようにする仕組みの導入
  • ドキュメント整備

辺りをやっていきたいと思っています.