LoginSignup
14
13

More than 5 years have passed since last update.

FRPNowでFizzBuzzなど

Posted at

ICFP2015に論文が出るのだそうで、局所的に話題になっているFRPNowというライブラリ。
ちょっと触ってみたので、感触などレポート。

とりあえずFizzBuzz

{-# LANGUAGE MultiWayIf #-}

module Main where

import Control.FRPNow
import Data.Functor ((<$), (<$>))
import Control.Monad (join)     

streaming ::
    (EvStream a -> Behavior (EvStream b, Event ())) ->
    IO a ->
    ([b] -> IO ()) ->
    IO ()
streaming b ioA ioB = runNowMaster $
  do
    (esA, cbA) <- callbackStream
    (esB, evE) <- sampleNow (b esA)
    loop cbA esB
    return evE

  where
    loop cbA esB =
      do
        eys <- sampleNow (nextAll esB)
        _ <- async (ioA >>= cbA)
        eev <- planNow $ async . ioB <$> eys
        planNow $ loop cbA esB <$ join eev
        return ()


mainB ::
    EvStream () -> Behavior (EvStream String, Event ())
mainB inEs = 
  do
    countUp <- foldEs (+) 0 (1 <$ inEs)
    end <- when ((>100) <$> countUp)
    return (toFizzBuzz countUp `snapshots` inEs, end)
  where
    toFizzBuzz :: Behavior Int -> Behavior String
    toFizzBuzz bX =
      do
        x <- bX
        let fizz = x `mod` 3 == 0
            buzz = x `mod` 5 == 0
        return $ if
            | fizz && buzz -> "FizzBuzz"
            | fizz -> "Fizz"
            | buzz -> "Buzz"
            | otherwise -> show x

main = streaming mainB (return ()) (mapM_ putStrLn)

streaming関数は汎用的に書いたドライバで、ロジック部分はmainBに集約されている。ロジックの表現としては、それなりに良い感じなのではないか、と思う。

FRPの基礎概念

ここでFRPの基礎をなす概念、EventとBehaviorについて簡単に触れる。

Behavior
時間に伴って変化する値。Behavior aはすなわちTime -> aの意。
Event
時間の中の一点にある値。Event aはだいたい(Time, a)。

なお、このEventの定義はSodiumやYampaといった既存のFRPと少し異なる。SodiumやYampaのEventは時系列上に何回でも発生する事ができる(式で表すとEvent a 〜 [(Time, a)]みたいな感じ)のに対し、FRPNowのEventは一度きりしか起きない。どちらかというとこれは、他の言語のpromise/futureに近いものであり、実際そういう使い方ができる事を後で示す。

FRPNowでは、普通のFRPのEvent aに相当するものとして、EvStream aという型が別途定義されている。

mainBの定義

それを踏まえるとmainBは、時間に伴って流れてくる()の列を受け取り、Stringの列と一つのイベントのペアを返す関数である事が分かる……と言いたい所だが、戻り値がBehaviorに包まれているのは一体何であろうか。

実を言うと、概念的には戻り値は(EvStream String, Event ())でも問題ないのだが、foldEsやwhenという関数がBehaviorに包まれた値を返してくるので、mainBもそれに伴ってBehaviorに包んだ値を返さざるを得ないのである。

なぜ余計なBehaviorで包むのかという、この点こそFRPNowの元論文の肝なので、興味のある方は直接読んで頂くのをお薦めする。だが、実プログラミング上でこの点を気にする必要はあまりない。Behaviorはモナドになっているので、returnや>>=を使えば簡単にBehavior型の値が作れるし、実行段階(後述するNowモナドの中)においてはいつでも、sampleNowという関数でBehaviorを剥がす事が可能だからだ。

さて、mainBの内容だが、()の列を受け取ると、まずはfoldEs関数を使ってcountUpというBehaviorを構築している。

foldEs :: (a -> b -> a) -> a -> EvStream b -> Behavior (Behavior a) 

これは初期値aに対し、ストリームの値を次々に二項演算で適用していったBehaviorを示す。
開始時点では0、それから()が一つ来る度に0+1、0+1+1、0+1+1+1という具合に数字が増えていくのである。

あとはロジックに従ってcountUpをFizzBuzz文字列に直し、それをsnapshotsでストリームに直したものを第一の戻り値としている。

第二の戻り値は停止条件を表す。

end <- when ((>100) <$> countUp)

という、いかにもそのままの定義だが、「countUpが100を超える瞬間」を第二の戻り値としている。

streamingの定義

streaming関数は、駆動すべきストリームb、入力値を受け取るためのアクションioA、出力値を処理するためのアクションioBを受け取り、bの第二戻り値が示す終端イベントが発生するまでの間、延々と処理を繰り返す関数である。

ioAに、たとえばgetLineでも渡してやる事によって、インタラクティブなCUIアプリケーションも作れるようにしたのだが、FizzBuzzは入力を取らないので、main関数では単に(return ())を渡し、延々と()を流すだけの入力ストリームとしている。出力にはputStrLnで画面に表示する処理を渡す。

そして、streaming関数の処理のキーとなるのはFRPNowで新星の如く現れた新概念、Nowモナドである。

NowモナドはFRPNowにおいて唯一の、外界と繋がる事ができるモナドであるが、Nowモナドのチェーンに含まれるものは全て同時に行われたとみなす点がIOモナドと異なる。このために、時間のかかるIOアクションの実行にはasyncを用いる。

async :: IO a -> Now (Event a) 

ここで思い出して頂きたいのは、「Eventはpromise/futureに似ている」という話である。まさしく、asyncの実行結果は他言語でいうところのfutureであるEvent型に包まれて返ってくる。

となるとここに、futureに続く処理を登録するような何かがあるはずであるが、これは以下の関数を使用する。

planNow :: Event (Now a) -> Now (Event a) 

Eventに包んだアクションを「予約」して、その戻り値をまたEventとして受け取る訳である。

streamingのloop関数では、まず出力ストリームesBの「次の値」nextAllの取得を予約したあと、ioA>>=cbAをasyncに実行している。cbAは値を入力ストリームに渡すためのコールバック関数であり、これによってシグナルネットワークが稼働して出力ストリームesBから値が出てくる。これらの非同期処理の結果はeysに入ってくるので、さらに非同期アクションioBの実行を予約。それが済んだらloopを再度実行、以下繰り返す。

「次の値」nextAllは、もちろん実行のその時々によって変化するから、その戻り値はBehavior (Event [a])という風に、時間変化するイベントになっている。Nowモナドの中では、Behaviorの「現時点での値」をsampleNowで取得する事ができる。loopの呼び出し時刻は、planNowによって少しずつ未来に遅延されていくから、sampleNowで取得できる値も少しずつ未来のものになる。これによって、ストリームの値を時間を時系列順に列挙できる、という仕組みである。

他のFRPとの比較

先ほど、色々なものがわざわざBehavior型に包まれて返ってくるのは気にする必要はない、というくだりがあったが、実はこの点は他のFRPライブラリと比べてちょっとしたアドバンテージになっている。

例えば、SodiumでfoldEsに相当するaccum関数の定義は以下の通り。

accum :: a -> Event (a -> a) -> Reactive (Behavior a) 

戻り値はReactive型(一般的な論文ではgeneratorと称されるもの)に包まれているが、これはFRPNowのNow型に相当し、IOアクションの実行が可能である(但し非同期実行の機能はなく、IOアクションは同期的に実行される)。こういう「副作用はないが、直近の入力値以上の何かに依存する」形のシグナル関数を、わざわざIOアクションが可能なモナドに包んで返すというのは、純粋性という観点からちょっと問題に感じる。

そこで、こういったものをgeneratorという概念を使わず、かつ既存のBehavior型で表現できるというのは、なかなか良いアイデアなのではないだろうか。最初に挙げた論文のredditコメントでも、その点について少し議論が行われている(同じ物が違う文脈で出てきて分かりづらいよ、というツッコミもあるが)。

まとめ

  • FRPNowは既存のFRPと同様にBehaviorやEventの概念を扱えるが、Behavior/Event層と生IO層の間にpromise/futureのような層があるのが面白い
  • いろいろなものがBehaviorに包まれて返ってくるが、慣れればどうという事はないしむしろ便利
14
13
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
13