2014-11-06 40 views
10

我正在學習反應式香蕉。爲了理解圖書館,我決定實施一個虛擬應用程序,只要有人按下按鈕就會增加計數器。我們爲什麼要在FRP中使用行爲

我使用的UI庫是Gtk,但與解釋無關。

這裏是非常簡單的實現,我想出了:

import Graphics.UI.Gtk 
import Reactive.Banana 
import Reactive.Banana.Frameworks 

makeNetworkDescription addEvent = do 
    eClick <- fromAddHandler addEvent 
    reactimate $ (putStrLn . show) <$> (accumE 0 ((+1) <$ eClick)) 

main :: IO() 
main = do 
    (addHandler, fireEvent) <- newAddHandler 
    initGUI 
    network <- compile $ makeNetworkDescription addHandler 
    actuate network 
    window <- windowNew 
    button <- buttonNew 
    set window [ containerBorderWidth := 10, containerChild := button ] 
    set button [ buttonLabel := "Add One" ] 
    onClicked button $ fireEvent() 
    onDestroy window mainQuit 
    widgetShowAll window 
    mainGUI 

這只是轉儲結果的殼。我想出了這個解決方案,閱讀Heinrich Apfelmus的article。請注意,在我的示例中,我沒有使用過一個Behavior

在文章有一個網絡的一個示例:

makeNetworkDescription addKeyEvent = do 
    eKey <- fromAddHandler addKeyEvent 
    let 
     eOctaveChange = filterMapJust getOctaveChange eKey 
     bOctave = accumB 3 (changeOctave <$> eOctaveChange) 
     ePitch = filterMapJust (`lookup` charPitches) eKey 
     bPitch = stepper PC ePitch 
     bNote = Note <$> bOctave <*> bPitch 
    eNoteChanged <- changes bNote 
    reactimate' $ fmap (\n -> putStrLn ("Now playing " ++ show n)) 
       <$> eNoteChanged 

該示例示出了stepper,它可將EventBehavior和使用changes帶回一個Event。在上面的例子中,我們只能使用Event,我想這不會有什麼區別(除非我不理解某些東西)。

那麼有人可以說明何時使用Behavior,爲什麼?我們是否應該儘快轉換所有Event

在我的小實驗中,我看不到Behavior可以在哪裏使用。

感謝

回答

6

任何時候FRP網絡都會在反應性香蕉中「做些什麼」,這是因爲它對某些輸入事件有反應。在系統外部進行任何可觀察的事情的唯一方式是通過連接外部系統對其產生的事件做出反應(使用reactimate)。

因此,如果您所做的只是通過生成輸出事件立即對輸入事件做出反應,那麼不會,您將找不到使用Behaviour的很多理由。

Behaviour是生產依賴於多個事件流,在那裏你必須記住,在事件不同時間發生程序的行爲非常有用的。

發生了Event;具體價值的具體時刻。 A Behaviour在所有時間點都有價值,沒有特殊的時間點(changes除外,這很方便,但有點破壞了模型)。

一個很簡單的例子是,如果我想對鼠標點擊做出反應,並且在沒有保持shift鍵的情況下進行shift-click操作,則會執行與點擊不同的操作。 Behaviour保持一個值,指示是否按住shift鍵,這很簡單。如果我只有Event用於換檔鍵的按下/釋放以及鼠標點擊,那就更困難了。

除了更難,它是更低的水平。爲什麼我必須做複雜的擺弄才能實現一個簡單的概念,如shift-click?在BehaviourEvent之間的選項對於實現您的程序概念來說是一個有用的抽象概念,它更貼近您在編程世界之外對它們的看法。

這裏的一個例子是遊戲世界中的可移動物體。我可能有一個Event Position代表它移動的所有時間。或者我可以只有一個Behaviour Position代表它在任何時候的位置。通常我會將這個對象看作一個位置,所以Behaviour是一個更好的概念擬合。

另一個地方Behaviour s是有用的代表外部觀察您的程序可以做,只能檢查「當前」值(因爲外部系統不會通知你,當發生變化)。

例如,假設您的程序必須在溫度傳感器上貼上標籤,並避免在溫度過高時啓動作業。通過Event Temperature,我將決定多頻繁輪詢溫度傳感器(或迴應什麼)。然後在我的其他例子中遇到與手動做某些事情相同的問題,以便使事件中最後的溫度讀數可用,從而決定是否開始工作。或者我可以使用fromPoll來製作Behaviour Temperature。現在我已經得到了一個代表溫度隨時間變化的值,並且我完全從投票傳感器中抽象出來;反應型香蕉本身可以根據需要頻繁地輪詢傳感器,而無需爲此需要任何邏輯!

+0

能否請您詳細說明「無香蕉本身需要輪詢的照顧傳感器經常因爲它可能需要」?它怎麼知道IO操作需要被查詢的頻率? – arrowd 2015-05-03 21:08:41

+0

@arrowdodger由於事情只是「發生」以響應事件,行爲在取決於事件的事件之間發生的實際值完全不相關。所以基本上,只要發生依賴事件就運行輪詢操作就足夠了(我曾經聽說,反應型香蕉實際上比以前更頻繁地進行輪詢;每當發生任何*輸入事件時)。因此,本質上它仍然處於「事件狀態」之下,但與我們將溫度傳感器建模爲事件不同,我們在定義它時不必修正採樣策略。 – Ben 2015-05-04 11:01:37

+0

謝謝,就像我最初想的那樣。 – arrowd 2015-05-04 11:04:49

7

Behavior■找一個值的時候,而Event唯一有在某一時刻的值。

想象一下,就像您在電子表格中一樣 - 大多數數據都以穩定的值(行爲)存在,並在必要時隨時更新。 (但是,在FRP中,依賴關係可以在沒有循環引用問題的任何一種情況下進行 - 數據從更改後的值更新爲不變的數據。)您還可以添加在按下按鈕或執行其他操作時觸發的代碼,但大多數數據始終可用。

當然,只要事件發生,你可以做所有事情 - 當發生變化時,閱讀這個值和那個值並輸出這個值,但它只是聲明式地表達這些關係並讓電子表格或編譯器擔心什麼時候更新東西爲你。

stepper用於改變發生在單元格中的值,change用於觀察單元格並觸發操作。輸出爲命令行上的文本的示例不受持久數據缺乏特別影響,因爲無論如何輸出都會突發。

但是,如果您有圖形用戶界面,與FRP模型相比,事件專用模型儘管可能並且確實很常見,但有點麻煩。在FRP中,您只需指定事物之間的關係而不必更新顯式。

這不是需要有行爲,類似地,你可以編寫一個Excel電子表格完全在VBA中沒有公式。數據持久性和等式規範更好。一旦你習慣了新的範例,你不會想回到手動追逐依賴和更新東西。

3

當你只有1 Event,或多個事件同時發生,或同一類型的多個活動,很容易只是union或以其他方式將它們組合成一個結果事件,然後傳遞給reactimate,並立即將其輸出。但是如果你有兩種不同類型的事件發生在不同的時間呢?然後將它們組合成可以傳遞給reactimate的結果事件成爲不必要的複雜因素。

我建議您從FRP explanation using reactive-banana實際嘗試和實現合成器,僅使用Events和no Behaviors,您會很快看到Behaviors簡化了不必要的事件操作。假設我們有2個事件,輸出Octave(類型爲Int的同義詞)和Pitch(類型爲Char的同義詞)。用戶按下的鍵從一個到克設置當前的俯仰,或按壓+-遞增或遞減當前八度音。該程序應輸出當前音高和當前八度音,如a0,b2f7。比方說,用戶在不同的時間按不同的組合這些鍵,所以我們結束了與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操作,例如getCharputStrLn(例如,後者的類型爲String -> IO())。 Handler類型的函數會獲取一個值並使用它執行一些IO計算。因此它只能在IO環境中使用(例如在main中)。

從類型很明顯(如果你瞭解單子的基礎知識)是fromAddHandlerreactimate只能在Moment上下文(例如makeDescriptionNetwork)使用,而newAddHandlercompileactuate只能在IO上下文(例如main)一起使用。

您創建的一對main使用newAddHandler類型AddHandlerHandler值,你通過這個新的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,我們想增加和減少數值並在按鍵之間記住它!所以我們需要accumEaccumE接受(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() 
相關問題