Subscribed unsubscribe Subscribe Subscribe

やさしいFunctional reactive programming(Behavior編)

Haskell FRP

注意: このエントリは書きかけです。

前回HaskellのFRPライブラリのreactiveについて取り上げ、Eventの使い方を紹介しました。今回はFRPにおいてEventと並んで重要なBehaviorを見ていきます。

Behaviorとは

Behaviorについては前回触れましたが、再度おさらいしておきます。Behavior とEvent の意味は、それぞれ次のように与えられます。

Behaviorは時間の関数です。つまり時間とともに変化する値を表現できます。Behaviorの最大の特徴はそれが連続的な時間を扱えることです。Eventは特定の時間とそのときの値をペアにしたものなので、扱う時間は離散的ですが、Behaviorは任意の時間の値が得られます。連続的な時間を扱うことでプログラムがシンプルになります。このメリットについてはConal Elliott » Blog Archive » Why program with continuous time?に良くまとまっています。

ではBehaviorの性質と使い方を理解するためにサンプルコードを見ていきましょう。

正弦波のアニメーション(SinWave.hs)

連続的に変化する値として正弦波を挙げます。ターミナルに正弦波を描いてみましょう。

まずは一番単純なBehaviorであるtime :: Behavior TimeIを使ってみます。ソースコードはTime.hsです。timeは時間を受け取り時間を返す、いわばid関数です。

import FRP.Reactive
import FRP.Reactive.LegacyAdapters

main :: IO ()
main = adaptE $ print <$> time `snapshot_` atTimes [0..]

このプログラムは1秒ごとに端末に時間*1を表示します。注目すべきポイントはsnapshot_です。型と名前から推測できるように、BehaviorをEvent発火のタイミングでサンプリングしています。上の例では、時間を表すtimeを0秒から1秒ごとにサンプリングして、出てきた値をEventとして返しています。出力部分は前回のEventで取り上げた例と全く同じです。

この例からわかるように、Behaviorは連続的な時間を扱いますが、出力などプログラムの最終段階では離散的な時間へと変換されます。

それでは、正弦波はどう表されるのか見てきます。

import FRP.Reactive
import FRP.Reactive.LegacyAdapters
import Control.Applicative

sinB :: Behavior Double
sinB = sin <$> time

draw :: Behavior Double -> Scale -> Behavior String
draw d s = replicate <$> (truncate <$> scaled) <*> pure '#'
  where scaled = pure s * (d + 1)

type Scale = Double

sinWave :: Scale -> Behavior String
sinWave = draw sinB

main :: IO ()
main = adaptE $ putStrLn <$> sinWave 30 `snapshot_` atTimes [0, 0.2 ..]

sin <$> timeのところに注目です。前回のEvent同様、BehavirもApplicativeのインスタンスになっています。従って浮動小数点数上の関数sinをApplicative functorを使ってBehavior上の計算に持ち上げることができます。<$>を使うと、Behaviorの計算をまるで普通の関数適用をしているかのように自然に書けることがわかります。

*Main> :t sin
sin :: (Floating a) => a -> a
*Main> :t (sin <$>)
(sin <$>) :: (Floating a, Functor f) => f a -> f a
*Main> :t (sin <$>) :: Floating a => Behavior a -> Behavior a
(sin <$>) :: Floating a => Behavior a -> Behavior a
  :: (Floating a) => Behavior a -> Behavior a

関数drawは正弦値Behavior Doubleを棒グラフに変換するところです。棒グラフの棒は"####"というようなStringで表現できるので、Behavior Stringであることがわかります。

実際に動かしてみましょう。

maoe@maoe ~/coding/haskell/reactive-examples
$ ghc --make SinWave.hs
[1 of 1] Compiling Main             ( SinWave.hs, SinWave.o )
Linking SinWave ...
maoe@maoe ~/coding/haskell/reactive-examples
$ ./SinWave 
##############################
###################################
#########################################
##############################################
###################################################
#######################################################
#########################################################
###########################################################
###########################################################
###########################################################
#########################################################
######################################################
##################################################
#############################################
########################################
##################################
############################
######################
################
###########
#######
###
#


#
###
######
###########
################
#####################

綺麗に正弦波を描くことができました。

インタラクティブなBehavior(SinWaveI.hs)

続いてBehaviorとユーザの入力を連携させてみます。今度はプログラムを起動してから、ユーザが何かキー入力するたびに、下記の挙動を切り替えていくというものです。

  1. 正弦波を描く
  2. 定数を描く
  3. 別の定数を描く

コードを見てみましょう。

import FRP.Reactive
import FRP.Reactive.LegacyAdapters
import Control.Applicative
import System.IO
import Control.Concurrent
import Control.Monad
import Data.Monoid
import Data.List

sinB :: Behavior Double
sinB = sin <$> time

type Scale = Double

draw :: Behavior Double -> Scale -> Behavior String
draw d s = replicate <$> (truncate <$> scaled) <*> pure '#'
  where scaled = pure s * (d + 1)

sinWave :: Event () -> Behavior Double
sinWave ev    = sinB `switcher` (next  <$ once ev)
  where next  = 0    `switcher` (next' <$ once (restE ev))
        next' = 1    `switcher` (loop  <$ once (restE (restE ev)))
        loop  = sinWave $ restE $ restE $ restE ev

sinWave' :: Event () -> Behavior Double
sinWave' = cycleB [ sinB, 0, 1 ]

-- cycle bsとiterate (once . restE) evで書き直したけどうまく動かなかった
cycleB :: [Behavior a] -> Event e -> Behavior a
cycleB bs ev = loop ev bs
  where loop e []      = cycleB bs e
        loop e (b:bs') = b `switcher` (loop (restE e) bs' <$ once e)

main :: IO ()
main = do
  hSetBuffering stdin NoBuffering
  hSetBuffering stdout NoBuffering
  hSetEcho stdin False
  (sink, event) <- makeEvent =<< makeClock
  forkIO $ forever $ getChar >> sink () >> putStrLn "A key is pressed"
  adaptE $ putStrLn <$> draw (sinWave' event) 30 `snapshot_` atTimes [0, 0.2 ..]

sinWaveが所望の動作を表現しているところです。インタラクティブなBehaviorの要であるswitcher関数が出てきました。switcherの型は次のようになっています。

switcher :: (Ord tr, Bounded tr) => BehaviorG tr tf a -> EventG tr (BehaviorG tr tf a) -> BehaviorG tr tf a

ドキュメントにはわかりやすくこう書かれています。

switcher :: Behavior a -> Event (Behavior a) -> Behavior a

switcherは、初期値となる最初のBehaviorと、次のBehaviorを値にもつEventを受け取り、それらを合成したBehaviorを返します。sinWaveでは初期BehaviorはsinBで、続くBehaviorはnextとなっています。onceというのはEventをリストと考えると、headに相当するものと考えてください。またrestEはtail相当です。

あとで続きを書く

積分(Integral.hs)

後で書く

import FRP.Reactive
import FRP.Reactive.LegacyAdapters
import Control.Applicative

velocity1 :: Behavior Double
velocity1 = 1

position :: Behavior Double
position = integral (atTimes [0..]) velocity1

main :: IO ()
main = adaptE $ print <$> position `snapshot_` atTimes [0,2..]

インタラクティブな積分(IntegralI.hs)

後で書く

import FRP.Reactive
import FRP.Reactive.LegacyAdapters
import Control.Applicative
import System.IO
import Control.Concurrent
import Control.Monad
import Data.Monoid

position :: Event Char -> Behavior Double
position ev = integral rate (velocity ev)
  where rate = everySecE

velocity :: Event Char -> Behavior Double
velocity ev = 0 `accumB` (controller <$> ev)

controller :: Char -> (Double -> Double)
controller '>' = (+) 0.1
controller '<' = flip (-) (0.1)
controller '0' = const 0
controller  _  = id

main :: IO ()
main = do
  hSetBuffering stdout NoBuffering
  hSetBuffering stdin  NoBuffering
  hSetEcho stdin False
  (sink, event) <- makeEvent =<< makeClock
  forkIO $ forever $ getChar >>= sink
  adaptE $ print <$> position event `snapshot_` everySecE

everySecE = atTimes [0..]

*1:reactiveで扱う時間は絶対的な時刻ではなく、プログラム起動時からの相対的な時間(秒数)です。