Arrowによるストリーム処理
(主にauto使用)

自己紹介


アズなんとかさん
 職業C++er
 日曜Haskeller
 同人SS書き

所属/関係サークル
・RedTailCat
・暗黒定数式(Vol2はお休み)


@as_capabl (twitter)
https://github.com/as-capabl
https://hackage.haskell.org/user/HidenoriAzuma

発表内容

  • 1. 導入
  • 2. Arrowの基礎
  • 3. Autoを使ってみる
  • 4. さらにArrowとストリームについて

1. 導入

前提

ストリームTransducerは
Category(圏)をなす

ストリームTransducerはCategory(圏)をなす

Transducer:
 pipes/conduit/machinesで
 SourceとかSinkとかPipeをひっくるめた物を
 便宜的にこう呼ぶ

pipes etcはTransducerを直列につないで何かするもの
  例. 入力 >>> フィルタ >>> 出力

直列つなぎは射の合成
=>TransducerはCategory(圏)である!!!

Categoryだと何ができるの?

Categoryだと何ができるの?

直列つなぎ。得意。

Categoryだと何ができるの?

並列つなぎ。
Categoryの枠外。

fanout(machines)などで何とか。

Categoryだと何ができるの?

??????

……いやいや

こんなストリーム何に使うんですか

例えば……

出力先が複数

エラーメッセージ出力

GUI

ドラッグアンドドロップも付けよう

Categoryでは力不足

なのでArrowを導入します

その前に

補足1 Categoryについて

あまり知られていないが、Categoryクラス(baseパッケージ)が存在する

pipes, conduit, machines: instance宣言はない (型シグネチャの関係で)
Arrow系(以降で紹介): 宣言している (Category a => Arrow aなので)

class Category cat where
id :: cat a a
(.) :: cat a b -> cat b c -> cat a c

補足2 pipesで並列グラフ

pipesのspawnで
一応可能。

できる事はAkka streamやLINQ/Rxとほぼ同じ。

スレッドとTVarのコストが掛かります。

2. Arrowの基礎

Arrowを導入しよう

Arrowとは?

Arrowとは?

原論文
"Generalising Monads to Arrows"
John Hughes, 1998

http://www.cse.chalmers.se/~rjmh/Papers/arrows.pdf

Arrowとは?

CategoryとMonadの
間にある型クラス


注:MonadからArrow, Categoryを作るには
Kleisliを使用

Arrowとは?

Categoryのメンバ
・id
・(.), (<<<), (>>>)
Arrowのメンバ
・arr
・first
・second
・***
・&&&
ArrowLoopのメンバ
・loop
(オプション)

……多い

けれど、要点は2つだけ

ひとつ目

arr

arr

関数をアローに持ち上げる。

ストリームの機能としてはよくある。


右図の見方:○は純関数、□はアロー(Transducer)


class Arrow ar where
arr :: (a->b) -> ar a b
...

ふたつ目

first

first

アローの横に「迂回路を作る」イメージ。

ストリームの機能としては、実を言うとかなり非自明。(後述)


class Arrow ar where
...
first :: ar a c -> ar (a, b) (c, b)
...

arrとfirstがあれば

あとは適宜

あとは適宜


second sf =
arr swap >>>
first sf >>>
arr swap

sf1 *** sf2 =
first sf1 >>> second sf2

sf1 &&& sf2 =
arr (\x->(x,x)) >>>
(sf1 *** sf2)

これを踏まえ

←があれば→を作れる?

まあ無理ではない

が、

正直つらそう

そこで

アロー記法

アロー記法

proc (全体の入力) ->
do
(出力先) <- (アロー) -< (入力元)

ghc拡張。{-# LANGUAGE Arrows #-}で使用可能に。

パッと見た感じ……
 ラムダ式の\がprocになったようなもの
 の後ろに
 モナドのdo文に謎の-<記号が入ったようなもの

アロー記法

例:普通の関数をアロー記法で定義
 a->bもArrowなのでこれが可能
 注:returnA == id == arr id

f :: Int -> Int
f = proc x ->
do
y <- arr (*2) -< x
returnA -< y
-- f 1 => 2

これを使えば……

これを使えば……

proc input ->
do
(a1, a2) <- a -< input
(b1, b2) <- b -< a2
rec
d1 <- d -< (b1, e1)
e1 <- e -< d1
(c1, c2) <- c -< (d1, b1)
f -< a1
g1 <- g -< c2
h -< g1
i -< c1

有向グラフをそのまま書ける

だが一方で

モナドのdoにも似ている?

モナドのdoにも似ている?

実は(見た目を除けば)違いは一点しかない

 Arrow記法は『-< の左側に一時変数を含められない』

(手前のスライドでは省略しましたが、そういう制約があります)

モナドのdoにも似ている?

ArrowApplyインスタンスがあると、
左に一時変数を入れる事も可能に
 =>もうArrow記法の意味ないですね!?

f x = proc _ ->
do
r <- getLineA -< ()
putStrLnA (x ++ r) -<< ()
-- getLineA, putStrLnAは架空の関数(でも簡単に定義可能)

モナドで出来る事は
モナドに任せましょう

では

Arrowの使い道は?

Arrowの使い道は?

  • パーサ
  • ストリーム処理
  • リアクティブプログラミング

3.Autoを使ってみる

Arrowでストリーム処理をしよう

……それで、

どのライブラリを使おうか

どのライブラリを使おうか

  • streamproc (Arrow原論文の例を実装したもの)
  • netwire ほかFRPライブラリ
  • machinecell (拙作)
  • auto

autoを使おう

autoを使おう

streamproc
  -- ちょっと問題がある(後述)

netwire ほかFRPライブラリ
  -- 本来の用途の外=>いらない所、足りない所が

machinecell (拙作)
  -- ver3に向けて工事中

autoとは

autoとは


https://hackage.haskell.org/package/auto

2015年3月リリース
netwireに"インスパイア"された
ストリーム特化のライブラリ

いろいろ試してみる

関数適用

関数適用

リストにarrを適用


ghci> streamAuto (arr (*2)) [1..10]
[2,4,6,8,10,12,14,16,18,20]

集積

集積

scanl (+) 0 [1..10]と同じ

ghci> streamAuto (accum (+) 0) [1..10]
[1,3,6,10,15,21,28,36,45,55]

集積

もちろん合成可能

ghci> streamAuto (arr (*2) >>> accum (+) 0) [1..10]
[2,6,12,20,30,42,56,72,90,110]

集積

アロー記法でも

ghci> streamAuto
(proc x -> do {y <- arr (*2)-< x; accum (+) 0 -< y})
[1..10]
[2,6,12,20,30,42,56,72,90,110]

集積

これでもOK

ghci> streamAuto
(proc x -> do {accum (+) 0 -< x * 2})
[1..10]
[2,6,12,20,30,42,56,72,90,110]

副作用

副作用

arrMで副作用
注:Arrowのメンバではなく、Auto独自の関数

ghci> streamAuto (arr show >>> arrM putStrLn) [1..10]
1
2
3
...

副作用

入力に使う場合
=>空の値を変換する

ghci> streamAuto
(arrM (\_ -> getLine) >>> arr reverse >>> arrM putStrLn)
(replicate 5 ())
foo
oof
...

副作用

シノニム: effect = arrM . const

ghci> streamAuto
(effect getLine >>> arr reverse >>> arrM putStrLn)
(replicate 5 ())
foo
oof
...

Blipストリーム

Blipストリーム

入出力は必ず 1-1 対応 -> filterとかできない

ので、Blip型を導入

・実装は単なるMaybe型
・1 - [0, 1]対応を実現,

Blipストリーム

3の倍数の時だけ出力
 emitOn - 条件に合うときだけ値を持つBlipストリームを作る
 arrMB - Blipが値を持たない時は実行しないarrM

ghci> import Control.Auto.Effects
ghci> streamAuto
(emitOn (\x -> x `mod` 3 == 0) >>> arrMB print)
[1..10]
3
6
9

FizzBuzzを書いてみる

FizzBuzzを書いてみる

fizzBuzz = proc x ->
do
let nowFizz = x `mod` 3 == 0
nowBuzz = x `mod` 5 == 0

fizzBuzz <- emitOn (==(True, True))
-< (nowFizz, nowBuzz)
fizz <- emitOn (==(True, False))
-< (nowFizz, nowBuzz)
buzz <- emitOn (==(False, True))
-< (nowFizz, nowBuzz)
showN <- emitOn (==(False, False))
-< (nowFizz, nowBuzz)

effectB $ putStrLn "FizzBuzz"
-< fizzBuzz
effectB $ putStrLn "Fizz" -< fizz
effectB $ putStrLn "Buzz" -< buzz
arrMB putStrLn -< show x <$ showN

4本の出力装置に振り分けるイメージ


注 (<$) = fmap . const
@Data.Functor FRP等でとてもよく出てくる

FizzBuzzを書いてみる

fizzBuzzCore = proc x ->
do
let nowFizz = x `mod` 3 == 0
nowBuzz = x `mod` 5 == 0
fizzBuzz <- emitOn (==(True, True))
-< (nowFizz, nowBuzz)
...

returnA -<
(fizzBuzz, fizz, buzz, x <$ showN)

main = streamAuto `flip` [0..100] $
proc x ->
do
(fizzBuzz, fizz, buzz, showNum) <-
fizzBuzzCore -< x

effectB $ putStrLn "FizzBuzz" -< fizzBuzz
effectB $ putStrLn "Fizz" -< fizz
...

コード分割

effectBの所を好きに変更可
(普通にやるとCallback hell)

Signal/Slotにちょっと似てる?

Local state

Local state

グラフはループさせることができる
=出力値を次の入力に使える
=局所的な状態の実現

fib = proc _ ->
do
rec x <- (delayN 2) 1 -< z
y <- (delayN 1) 1 -< z
let z = x + y
id -< x
-- streamAuto fib [1..5] => [1, 1, 2, 3, 5]

シリアライゼーション

シリアライゼーション

fib = proc _ ->
do
rec x <- (delayN 2) 1 -< z
y <- (delayN 1) 1 -< z
let z = x + y
id -< x
main =
do
Right fib0 <-
readAutoDef "save.dat" fib

let (r, fib') = stepAutoN' 5 fib0 ()
print r

writeAuto "save.dat" fib'

さらなる情報

https://hackage.haskell.org/package/auto
 (Readme, Haddockドキュメント)

http://blog.jle.im/entries/series/+all-about-auto
 (ブログ記事)

4. さらにArrowとストリームについて

ArrowはCategoryより高機能

ならばautoは
pipes等より高機能か?

...否

...否

autoは 1-[0, 1] 対応まで
      pipes等は 1-*(一対多)

1-* って必須なの?

1-* って必須なの?

 .. <- await
 yield ..
 yield ..

↑みたいなとき必然的に発生
=>コルーチンが実装可能であることの必要条件

(Arrowで1-*対応)
すればいいのでは?

……やってみると難しい

……やってみると難しい

例)
f :: Auto m () Int -- 1, 2を返す
g :: Auto m () Int -- 1, 2, 3を返す

このとき f&&&g :: Auto m () (Int, Int)は……
  (1, 1), (2, 2), (???, 3)

……やってみると難しい

https://hackage.haskell.org/package/streamproc
streamproc(原論文の実装)
 勝手にコピーしたり切ったりする


https://groups.google.com/forum/#!topic/haskell-pipes/H6YdVhyNksk
pipesの試験実装
 組み合わせを出力 (1, 1), (1, 2), (1, 3), (2, 1), (2, 2), (2, 3)


どちらもアロー記法にしたとき非直感的な動きになる

machinecellは……

machinecellは……


https://hackage.haskell.org/package/machinecell

拙作
元々はmachinesのArrow化を目指したライブラリ
途中でYampa/netwireを参考に
 =>autoとは従兄弟の関係

autoとの違い
 In) await/yield
 Out) Serialization

machinecellは……

個数問題の解決法:全部Event(autoでいうBlip)型にする。

run :: ProcessA a (Event b) (Event c) -> a [b] [c]
全体の入力/出力がEvent

construct :: Plan i o -> ProcessA a (Event i) (Event o)
Planモナド = await/yieldを含むコルーチンモナド
 =>oをyieldするとEvent oが出てくるイメージ

machinecellは……

先の例)
f :: ProcessA a (Event ()) (Event Int) -- 1, 2を返す
g :: ProcessA a (Event ()) (Event Int) -- 1, 2, 3を返す
f&&&g :: ProcessA a (Event ()) (Event Int, Event Int)
となるので

いらない所にNoEvent(MaybeでいうNothing)を詰めれば解決
 (Event 1, Event 1), (Event 2, Event 2), (NoEvent, Event 3)

まとめ

まとめ

Arrowでストリーム処理がしたい

-> Serializationが欲しい -> auto

-> await/yieldが欲しい -> machinecell

ご清聴ありがとうございました