尝试FPR

October 3, 2013

之前经常看到Functional Reactive Programm,看了一些库的文档,不知道是怎么实现的,不知道靠那几个基本的combinator能做什么,昨天试了一下threepenny-gui,突然觉得自己会用FRP了。

发现它有个Timer模块,我做的第一个尝试就是记录Timer事件出现的总次数。

ghci> import Reactive.Threepenny
ghci> import qualified Graphics.UI.Threepenny.Timer as Timer
ghci> import Control.Concurrent
ghci> import Control.Monad

首先, Timer的事件(流)是一个Event (),表示内容为(),因为这Timer只关心时间间隔到了。

通过stepper我们把一个Event转换成一个Behavior,然后用currentValue经过Behavior观察到Event当前的值。

ghci> timer <- Timer.timer
ghci> behavior <- stepper () (Timer.tick timer)
ghci> Timer.start timer
ghci> replicateM 20 $ currentValue behavior >>= print >>threadDelay(10^6)

会看到一串(),因为Timer事件的值一直都是()。再看accumE函数:

accumE :: a -> Event (a -> a) -> IO (Event a)

给定一个累加器的初始值和一个产生函数的事件流,返回一个新的事件流,这个新的事件流是把函数流做用到累加器上的得到的。假设0是初始值,函数流里都是(+1),那么accumE得到的事件流就是 0、(0 + 1)、 (0 + 1 + 1) … 。这么说,我们只要把Timer的事件流变成一个函数事件流,我们就可以记录事件次数。

apply :: Behavior (a -> b) -> Event a -> Event b

apply可以把一个Behavior作用到事件流上得到一个新的事件流,这和我们的想法很接近了。只要把const (+1)作用到Timer的事件流上就可以了。因为Behavior是个Applicative,我们直接用pure可以把一个函数转成Behavior。

ghci> let incEvent = apply (pure $ const ((+1)::Int->Int)) (Timer.tick timer)
ghci> countEvent <- accumE 0 incEvent

后来才发现Event是Functor,直接fmap就可以了

ghci> let incEvent = fmap (const ((+1)::Int->Int)) (Timer.tick timer)
ghci> countEvent <- accumE 0 incEvent

可是这样得到的countEvent还是个Event,还要用stepper转成Behavior。

ghci> count <- stepper 0 countEvent

然后就可以用currentValue count得到现在事件出现的总次数了。

可以看出通过applyfmap和Behavior实现事件流的转化,accumE实现事件累加,stepper实现Event到Behavior的转换。

为什么Behavior会独立存在,它好像也是个流而已?如果apply的类型变成Event (a -> b) -> Event a -> Evnet b,会发现两个事件流必须同步才可行,不然某个时间点内可能没有函数或者没有值。

Behavior就被用来表达一种保持值的流,在下一次改变前会保持当前值。FPR里一个概念就是Time-varying Value,根据时间而改变的值,在这就是用Behavior表示的,是连续的,Event表示离散的值。

参考

  1. Is the ‘Signal’ representation of Functional Reactive Programming correct?