読者です 読者をやめる 読者になる 読者になる

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で非常に簡潔に

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

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