當你只有1 Event,或多個事件同時發生,或同一類型的多個活動,很容易只是union
或以其他方式將它們組合成一個結果事件,然後傳遞給reactimate
,並立即將其輸出。但是如果你有兩種不同類型的事件發生在不同的時間呢?然後將它們組合成可以傳遞給reactimate
的結果事件成爲不必要的複雜因素。
我建議您從FRP explanation using reactive-banana實際嘗試和實現合成器,僅使用Events和no Behaviors,您會很快看到Behaviors簡化了不必要的事件操作。假設我們有2個事件,輸出Octave(類型爲Int的同義詞)和Pitch(類型爲Char的同義詞)。用戶按下的鍵從一個到克設置當前的俯仰,或按壓+或-遞增或遞減當前八度音。該程序應輸出當前音高和當前八度音,如a0
,b2
或f7
。比方說,用戶在不同的時間按不同的組合這些鍵,所以我們結束了與2個事件流(活動)這樣的:
+ - + -- octave stream (time goes from left to right)
b c -- pitch stream
每次用戶按下一個鍵,我們輸出電流倍頻和間距。但結果事件應該是什麼?假設默認音高是a
,默認倍頻程是0
。我們應該結束了,看起來像這樣的事件流:
a1 b1 b0 c0 c1 -- a1 corresponds to + event, b1 to b, b0 to -, etc
簡單的字符輸入/輸出
讓我們試着從頭實現合成器,看看我們是否可以不用行爲。讓我們先寫一個程序,你把一個字符,請按輸入,節目輸出,並再次道白:
import System.IO
import Control.Monad (forever)
main :: IO()
main = do
-- Terminal config to make output cleaner
hSetEcho stdin False
hSetBuffering stdin NoBuffering
-- Event loop
forever (getChar >>= putChar)
簡單事件的網絡
讓我們做以上,但與一個事件網絡來說明它們。
import Control.Monad (forever)
import System.IO (BufferMode(..), hSetEcho, hSetBuffering, stdin)
import Control.Event.Handler (newAddHandler)
import Reactive.Banana
import Reactive.Banana.Frameworks
makeNetworkDescription :: Frameworks t => AddHandler Char -> Moment t()
makeNetworkDescription myAddHandler = do
event <- fromAddHandler myAddHandler
reactimate $ putChar <$> event
main :: IO()
main = do
-- Terminal config to make output cleaner
hSetEcho stdin False
hSetBuffering stdin NoBuffering
-- Event loop
(myAddHandler, myHandler) <- newAddHandler
network <- compile (makeNetworkDescription myAddHandler)
actuate network
forever (getChar >>= myHandler)
網絡就是您所有的事件和行爲都存在並互相交流的地方。他們只能在Moment
monadic上下文中執行此操作。在教程Functional Reactive Programming kick-starter guide中,事件網絡的類比是人腦。人腦是所有事件流和行爲彼此交錯的地方,但唯一訪問大腦的方式是通過作爲事件源(輸入)的受體。現在
,在我們開始之前,仔細檢查了類型上面的代碼中的最重要的功能:
type Handler a = a -> IO()
newtype AddHandler a = AddHandler { register :: Handler a -> IO (IO()) }
newAddHandler :: IO (AddHandler a, Handler a)
fromAddHandler :: Frameworks t => AddHandler a -> Moment t (Event t a)
reactimate :: Frameworks t => Event t (IO()) -> Moment t()
compile :: (forall t. Frameworks t => Moment t()) -> IO EventNetwork
actuate :: EventNetwork -> IO()
因爲我們使用的最簡單的用戶界面可能 - 字符輸入/輸出,我們將使用模塊Control.Event.Handler
,由Reactive-banana提供。通常GUI庫爲我們做這個骯髒的工作。
Handler
類型的函數只是一個IO操作,類似於其他IO操作,例如getChar
或putStrLn
(例如,後者的類型爲String -> IO()
)。 Handler
類型的函數會獲取一個值並使用它執行一些IO計算。因此它只能在IO環境中使用(例如在main
中)。
從類型很明顯(如果你瞭解單子的基礎知識)是fromAddHandler
和reactimate
只能在Moment
上下文(例如makeDescriptionNetwork
)使用,而newAddHandler
,compile
和actuate
只能在IO
上下文(例如main
)一起使用。
您創建的一對main
使用newAddHandler
類型AddHandler
和Handler
值,你通過這個新的AddHandler
功能,您的活動網絡功能,在這裏你可以創建一個事件流出來它使用fromAddHandler
的。您儘可能多地操作此事件流,然後將其事件包裝在IO操作中,並將生成的事件流傳遞到reactimate
。
過濾事件
現在,讓我們只輸出的東西,如果用戶按下+或-。當用戶按+,當用戶按下-時,我們輸出1。 (其餘代碼保持不變)。
action :: Char -> Int
action '+' = 1
action '-' = (-1)
action _ = 0
makeNetworkDescription :: Frameworks t => AddHandler Char -> Moment t()
makeNetworkDescription myAddHandler = do
event <- fromAddHandler myAddHandler
let event' = action <$> filterE (\e -> e=='+' || e=='-') event
reactimate $ putStrLn . show <$> event'
我們如果用戶按下+或旁邊什麼不輸出 -,清潔的方法是:事件操作
action :: Char -> Maybe Int
action '+' = Just 1
action '-' = Just (-1)
action _ = Nothing
makeNetworkDescription :: Frameworks t => AddHandler Char -> Moment t()
makeNetworkDescription myAddHandler = do
event <- fromAddHandler myAddHandler
let event' = filterJust . fmap action $ event
reactimate $ putStrLn . show <$> event'
重要作用(見Reactive.Banana.Combinators
更多) :
fmap :: Functor f => (a -> b) -> f a -> f b
union :: Event t a -> Event t a -> Event t a
filterE :: (a -> Bool) -> Event t a -> Event t a
accumE :: a -> Event t (a -> a) -> Event t a
filterJust :: Event t (Maybe a) -> Event t a
積累增量和減量s
但是我們不想只輸出1和-1,我們想增加和減少數值並在按鍵之間記住它!所以我們需要accumE
。 accumE
接受(a -> a)
類型的值和函數流。每次從這個流中出現一個新函數時,它都會應用到該值,並記住結果。下一次出現一個新函數時,它將應用於新值,依此類推。這使我們能夠記住,我們目前不得不減少或增加哪個數字。
makeNetworkDescription :: Frameworks t => AddHandler Char -> Moment t()
makeNetworkDescription myAddHandler = do
event <- fromAddHandler myAddHandler
let event' = filterJust . fmap action $ event
functionStream = (+) <$> event' -- is of type Event t (Int -> Int)
reactimate $ putStrLn . show <$> accumE 0 functionStream
functionStream
基本上是功能(+1)
,(-1)
,(+1)
,這取決於密鑰的用戶按下一個流。
團結了兩個事件流
現在我們已經準備好實現從原來的文章都八度和俯仰。
type Octave = Int
type Pitch = Char
actionChangeOctave :: Char -> Maybe Int
actionChangeOctave '+' = Just 1
actionChangeOctave '-' = Just (-1)
actionChangeOctave _ = Nothing
actionPitch :: Char -> Maybe Char
actionPitch c
| c >= 'a' && c <= 'g' = Just c
| otherwise = Nothing
makeNetworkDescription :: Frameworks t => AddHandler Char -> Moment t()
makeNetworkDescription addKeyEvent = do
event <- fromAddHandler addKeyEvent
let eChangeOctave = filterJust . fmap actionChangeOctave $ event
eOctave = accumE 0 ((+) <$> eChangeOctave)
ePitch = filterJust . fmap actionPitch $ event
eResult = (show <$> ePitch) `union` (show <$> eOctave)
reactimate $ putStrLn <$> eResult
我們的程序將根據用戶按下的內容輸出當前音高或當前八度。它也會保留當前八度的值。可是等等!這不是我們想要的!如果我們要輸出當前音調和當前八度音,那麼每次用戶按下一個字母或+或-?
而這裏變得非常困難。我們不能合併不同類型的事件流,因此我們可以將它們都轉換爲Event t (Pitch, Octave)
。但是如果音高事件和八度事件發生在不同的時間(即它們不是同時的,在我們的例子中實際上確定),那麼我們的臨時事件流寧願有類型Event t (Maybe Pitch, Maybe Octave)
,Nothing
到處都沒有對應事件。因此,如果用戶按順序按下+ b-c +,並且我們假設默認倍頻程爲0並且默認音調爲a
,那麼我們最終得到一系列對[(Nothing, Just 1), (Just 'b', Nothing), (Nothing, Just 0), (Just 'c', Nothing), (Nothing, Just 1)]
,包裝在Event
中。
然後我們必須弄清楚如何用當前的音調或八度代替Nothing
,所以結果序列應該是[('a', 1), ('b', 1), ('b', 0), ('c', 0), ('c', 1)]
。
這太低級了,一個真正的程序員不應該擔心如果有高級抽象可用,就會像這樣對齊事件。
行爲簡化了事件處理
一些簡單的修改,我們取得了同樣的結果。
makeNetworkDescription :: Frameworks t => AddHandler Char -> Moment t()
makeNetworkDescription addKeyEvent = do
event <- fromAddHandler addKeyEvent
let eChangeOctave = filterJust . fmap actionChangeOctave $ event
bOctave = accumB 0 ((+) <$> eChangeOctave)
ePitch = filterJust . fmap actionPitch $ event
bPitch = stepper 'a' ePitch
bResult = (++) <$> (show <$> bPitch) <*> (show <$> bOctave)
eResult <- changes bResult
reactimate' $ (fmap putStrLn) <$> eResult
匝間距事件與stepper
行爲,並與accumB
取代accumE
獲得倍頻的行爲,而不是倍頻事件。要獲得最終的行爲,請使用applicative style。
然後,要獲得活動,您必須傳遞給reactimate
,將產生的行爲傳遞給changes
。但是,changes
會返回一個複雜的單值值Moment t (Event t (Future a))
,因此您應該使用reactimate'
而不是reactimate
。這也是爲什麼你必須在上面的例子中將putStrLn
兩次提到eResult
,因爲你將它提升到Event
仿函數中的Future
仿函數。
退房的類型,這裏我們用來了解什麼去哪裏的功能:
stepper :: a -> Event t a -> Behavior t a
accumB :: a -> Event t (a -> a) -> Behavior t a
changes :: Frameworks t => Behavior t a -> Moment t (Event t (Future a))
reactimate' :: Frameworks t => Event t (Future (IO())) -> Moment t()
能否請您詳細說明「無香蕉本身需要輪詢的照顧傳感器經常因爲它可能需要」?它怎麼知道IO操作需要被查詢的頻率? – arrowd 2015-05-03 21:08:41
@arrowdodger由於事情只是「發生」以響應事件,行爲在取決於事件的事件之間發生的實際值完全不相關。所以基本上,只要發生依賴事件就運行輪詢操作就足夠了(我曾經聽說,反應型香蕉實際上比以前更頻繁地進行輪詢;每當發生任何*輸入事件時)。因此,本質上它仍然處於「事件狀態」之下,但與我們將溫度傳感器建模爲事件不同,我們在定義它時不必修正採樣策略。 – Ben 2015-05-04 11:01:37
謝謝,就像我最初想的那樣。 – arrowd 2015-05-04 11:04:49