Reactive Programming in Perl6 その1

昨年のクリスマスにリリースされたPerl6であるが、一番の売りはなんなのかと言えば 並行、非同期処理がサポートされていることだと思う。

ということでPerl6でReactive Programmingができるのか見てみたい。

Reactive Programmingとは

まずReactive Programmingとは何か。僕としては

などを読み、

  • 時間とともに変化しうる値 = 時間順に並んだイベントの列 = ストリーム

を中心人物としてプログラミングする手法だと理解した。 ただ、時間とともに変化しうる値と言われても意味不明なので簡単な例を出したい。 以下の式を考える。

a = 2
b = 3
c = a + b

aに2を、bに3を代入し、そしてcをa+bと定義している。よってcは5である。 もし少し時間が経ってからa = 6と代入してもcは5のままである。

ここでReactive Programmingの立場に立って考えるとcは9になる。 すなわちReactive Programmingではc = a + bが「cは常にaとbの和である」の意味になる。

Reactive Programmingの立場で上記式をもっと説明的に書くとすれば

a = 時間とともに変化しうる値で初期値は2
b = 時間とともに変化しうる値で初期値は3
c = 時間とともに変化しうるaとbの和

(注: c自体も時間とともに変化しうる値である)

のようになる。この様子を(B)で使われている 横方向を時間の流れとして表すダイアグラムで書くと以下のようになる。

a: -- 2 --- 6 ----
b: -- 3 ----------
   vvvv [+] vvvvvv (aとbを足してcを作る)
c: -- 5 --- 9 ----
      ^     ^
      ^     a=6,b=3で9になる
      a=2,b=3で5になる

Perl6でどうやるか

ここからが本題で、Perl6でReactive Programmingはどうやるのか。具体的には上記の例

a = 時間とともに変化しうる値で初期値は2
b = 時間とともに変化しうる値で初期値は3
c = 時間とともに変化しうるaとbの和

# ダイアグラムで表すと...
a: -- 2 --- 6 ----
b: -- 3 ----------
   vvvv [+] vvvvvv (aとbを足してcを作る)
c: -- 5 --- 9 ----
      ^     ^
      ^     a=6,b=3で9になる
      a=2,b=3で5になる

をどのように書くのか。

結論を先に書けば以下である。なんと簡単か!

my $a = Supplier.new;
my $b = Supplier.new;
my $c = Supply.zip-latest: $a.Supply, $b.Supply, with => &[+];
$c.tap: { say $_ };
$a.emit(2);
$b.emit(3);
sleep 1;
$a.emit(6);

以下、まずSupplyについて説明する。

Supply

Perl6では

  • 時間とともに変化しうる値 = 時間順に並んだイベントの列 = ストリーム

のことをSupplyと呼ぶ。そしてSupplyに値をemitするものをSupplierと呼ぶ。

例を出そう。 Perl6ではシグナル(SIGINT, SIGTERM, ...)も「時間順に並んだイベントの列」であることから Supplyとして提供されている。

my $signal-supply = signal(SIGINT);
say so $signal-supply ~~ Supply; # True

シグナルの発生源はOSであることから$signal-supplyのSupplierは表立って出てこない。 一方、自分で「時間とともに変化する値」を作りたい場合は

my $supplier = Supplier.new;
my $supply   = $supplier.Supply; # $supplierが値をemitする先のSupply

# いろいろやった後...
$supplier.emit("value"); # $supplyに値がemitされる

のようにSupplierオブジェクトを使うことになる。

Supplyのtap

さてSupplyがあったときそれの値を読む(subscribe)にはどうすればいいのか。 tapすればよい。

my $signal-supply = signal(SIGINT);
$signal-supply.tap: -> $v { say "catch $v" };

sleep;

$signal-supplyに値がemitされる(つまりINTシグナルが送られる)と -> $v { say "catch $v" }が実行される。試しに上記コードを実行し Ctrl+Cを押してみてほしい。

Supplyの操作

tap以外にもSupplyにはいろいろなメソッドが用意されているが、 ここではzip-latestだけ説明する。

我々がやりたかったのは

a = 時間とともに変化しうる値(ストリーム)で初期値は2
b = 時間とともに変化しうる値(ストリーム)で初期値は3
c = 時間とともに変化しうるaとbの和

であった。時間とともに変化しうる値=Supplyとわかったからa, bはもうPerl6で書けそうだ。 cのために既存のSupplyからそれらを合成しまた新しいSupplyを作る必要がある。 より詳しくは

  • 複数のSupplyのどれか一つでも値がemitされたら、それらの最新の値を合成してemitするSupply

を作る必要がある。これはまさにzip-latestメソッドで作れる。zip-latestは

method zip-latest(Supply @*supplies, :&with = &[,], :$initial) returns Supply:D

で定義されるメソッドでwithで合成の方法を指定できる。

例に戻る

長々Supplyについて説明したが、例に戻る。 「時間とともに変化しうる値a,bの和c」をプログラムに落とし込むと

my $a = Supplier.new; # L1
my $b = Supplier.new; # L2
my $c = Supply.zip-latest: $a.Supply, $b.Supply, with => &[+]; # L3
$c.tap: { say $_ }; # L4
$a.emit(2); # L5
$b.emit(3); # L6
sleep 1; # L7
$a.emit(6); # L8

になるのだった。L1,L2で値をemitしうるSupplierを作っている。そして L3で$a.Supply、$b.Supplyからemitされる値を足し算&[+]によって合成した 新しいSupplyを作り$cに入れている。

基本的にこの3行で「時間とともに変化しうる値a,bの和c」は実現できた。 残りの行は実際に値を流してみてそれを確認しているだけである。 L4で$cの値を確認するため、STDOUTに表示するように設定し、 L5~L8で$a,$bの値を変えてみている。

結論

説明は長かったが、Perl6で非常に簡潔に

  • 時間とともに変化しうる値 = 時間順に並んだイベントの列 = ストリーム

を扱えることがわかった。次回は(もしあれば)、もっと実践的な例を扱いたい。

Execute an external program with timeout in Perl6

Proc::Async allows us to execute an external program asynchronously.

my $proc = Proc::Async.new("curl", "-s", "-o", "index.html", "http://www.cpan.org");
my $res = await $proc.start;

Can we use Proc::Async with a timeout? Proc::Async does not support it officially, but we can implement it easily. Take a look at this:

class Proc::Async::Timeout is Proc::Async {
    has $.timeout is rw;
    method start($self: |) {
        return callsame unless $.timeout;
        my $killer = Promise.in($.timeout).then: { $self.kill };
        my $promise = callsame;
        Promise.anyof($promise, $killer).then: { $promise.result };
    }
}

my $proc = Proc::Async::Timeout.new("perl", "-E", "sleep 5; warn 'end'");
$proc.timeout = 1;
my $res = await $proc.start;
say "maybe timeout" if $res.signal;

Wow! You could even do:

my $start = Proc::Async.^methods.first(*.name eq "start");

$start.wrap: method ($self: :$timeout, |c) {
    return callwith($self, |c) unless $timeout;
    my $killer = Promise.in($timeout).then: { $self.kill };
    my $promise = callwith($self, |c);
    Promise.anyof($promise, $killer).then: { $promise.result };
};

say await Proc::Async.new("perl", "-E", 'sleep 3; say "end"').start(timeout => 2);

Wow Wow! If you find cooler ways, please let us know!

Asynchronous http call by HTTP::Tinyish

Perl6 HTTP::Tinyish now can be called asynchronous way.

github.com

my $http = HTTP::Tinyish.new(:async);

my @url = <
  http://perl6.org/
  https://doc.perl6.org/
  http://design.perl6.org/
>;

my @promise = @url.map: -> $url {
  $http.get($url).then: -> $promise {
    my %res = $promise.result;
    say "Done %res<status> %res<url>";
    %res;
  };
};

my @res = await @promise;

HTTP::Tinyish is just a wrapper for curl.

I hope we have a stable http client written in pure Perl6, which supports asynchronous http calls.

released CPAN-Mirror-Tiny

I've just released CPAN-Mirror-Tiny to cpan!

metacpan.org

What's this?

This is a yet another DarkPAN manager.

Why do we need a new DarkPAN manager?

I used to use OrePAN2. I was satisfied with its functionality, but its dependencies on cpan modules increased more and more. Now OrePAN2 depends on 128 distributions including Moose.

I want to use DarkPAN in CPAN clients. Then minimal dependency and no dependency on XS modules is critical.

My new DarkPAN CPAN-Mirror-Tiny does not depend on XS modules, and depends on only 10 distributions. This is why I made a new DarkPAN manager.

Carl

I've just switched from OrePAN2 to CPAN::Mirror::Tiny in Carl, which is a yet anothor Carton.

If we want to make cpan clients support installing modules from any sources (cpan, http, git, ...), then it seems to be easy to create a local cpan mirror first, and install modules from the local cpan mirror. Carl is a proof of concept of that.

plenv download

plenvでprecompiled perl binaryをさっと使えるようにするpluginを書いた。

github.com

$ plenv download latest

もしネットワーク環境がよければ上記コマンドを打って10秒ほどで最新のperlが使えるようになるだろう。

ぜひお試しください。

perl6のコード例

昨年クリスマスにリリースされたと噂されているperl6はスコープを抜けた時に実行するコードをLEAVEでかける。例えばfile処理は

given "file.txt".IO.open(:w) -> $fh {
  LEAVE $fh.close;
  $fh.say("hello world!");
};

と書くとよい。また

{
  my %old = %*ENV;
  LEAVE { %*ENV = %old }
  do-something-with-ENV();
}

と書けばdo-something-with-ENVで何をやろうとスコープを抜けたあとは環境変数がもとに戻る。 ただ、この場合はperl5で言うところのlocalと同じ働きをするtempを使って

{
  temp %*ENV;
  do-something-with-ENV();
}

と書いた方がすっきりする。

さて、perl6 module mi6には

sub withp6lib(&code) {
  temp %*ENV;
  %*ENV<PERL6LIB> = "lib/";
  &code();
}

sub test() {
  withp6lib {
    run <prove -e perl6 t/>;
  };
}

というコードがある。これはtest()の時だけ環境変数PERL6LIBを設定するコードである。 これはこれでいいのだが、traitを使うとさらにカッコよくかけることに気づいた。

multi trait_mod:<is>(Routine $code, :$with-perl6lib) {
  $code.wrap: {
    temp %*ENV;
    %*ENV<PERL6LIB> = "lib/";
    callsame();
  };
}

sub test() is with-perl6lib {
  run <prove -e perl6 t/>;
}  

multi trait_modのところがtraitの定義であり、その内部では$codeをwrapし、環境変数PERL6LIBを設定している。 callsame()は$codeを実行するという意味である。

perl6は書けば書くほど新しい文法が発見される楽しい言語で書き味が素晴らしい。 実際のところ上記も、もっといい書き方があるのかもしれない。

もしperl6を実際書いてみたいという方がいるなら、perl6/roast, perl6/specs あたりをgit cloneして全文をgrep検索することをお勧めしたい。