之前经常看到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
得到现在事件出现的总次数了。
可以看出通过apply
、fmap
和Behavior实现事件流的转化,accumE
实现事件累加,stepper
实现Event到Behavior的转换。
为什么Behavior会独立存在,它好像也是个流而已?如果apply
的类型变成Event (a -> b) -> Event a -> Evnet b
,会发现两个事件流必须同步才可行,不然某个时间点内可能没有函数或者没有值。
Behavior就被用来表达一种保持值的流,在下一次改变前会保持当前值。FPR里一个概念就是Time-varying Value,根据时间而改变的值,在这就是用Behavior表示的,是连续的,Event表示离散的值。