2014-11-06 14 views
10

Tôi đang học chuối phản ứng. Để hiểu được thư viện, tôi đã quyết định triển khai một ứng dụng giả để tăng số lượt truy cập mỗi khi có ai đó nhấn nút.Tại sao chúng ta nên sử dụng Hành vi trong FRP

Thư viện giao diện người dùng tôi đang sử dụng là Gtk nhưng không liên quan đến giải thích.

Đây là việc thực hiện rất đơn giản mà tôi đã đưa ra:

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 

này chỉ bãi kết quả trong vỏ. Tôi đã đưa ra giải pháp này đọc số article của Heinrich Apfelmus. Lưu ý rằng trong ví dụ của tôi, tôi đã không sử dụng một đơn Behavior.

Trong bài báo có một ví dụ về một mạng:

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 

Ví dụ hiển thị một stepper mà biến đổi một Event thành một Behavior và mang lại một Event sử dụng changes. Trong ví dụ trên, chúng tôi chỉ có thể sử dụng Event và tôi đoán rằng nó sẽ không tạo ra sự khác biệt (trừ khi tôi không hiểu điều gì đó).

Vì vậy, ai đó có thể làm sáng tỏ thời điểm sử dụng Behavior và tại sao? Chúng tôi có nên chuyển đổi tất cả Event giây sớm nhất có thể không?

Trong thử nghiệm nhỏ của tôi, tôi không nhìn thấy nơi Behavior có thể được sử dụng.

Cảm ơn

Trả lời

6

Bất cứ lúc nào mạng FRP "làm điều gì đó" trong Reactive Banana đó là vì nó phản ứng với một số sự kiện đầu vào. Và cách duy nhất nó làm bất cứ điều gì quan sát được bên ngoài hệ thống là bằng cách nối dây một hệ thống bên ngoài để phản ứng với các sự kiện mà nó tạo ra (sử dụng reactimate). Vì vậy, nếu tất cả những gì bạn đang làm là phản ứng ngay lập tức với một sự kiện đầu vào bằng cách tạo ra một sự kiện đầu ra, thì không, bạn sẽ không tìm thấy nhiều lý do để sử dụng Behaviour.

Behaviour rất hữu ích cho việc tạo hành vi chương trình phụ thuộc vào nhiều luồng sự kiện, nơi bạn phải nhớ rằng các sự kiện diễn ra tại thời điểm khác nhau.

An Event có lần xuất hiện; những khoảnh khắc cụ thể về thời gian mà nó có giá trị. Một Behaviour có một giá trị tại tất cả các điểm trong thời gian, không có thời điểm nào là đặc biệt (ngoại trừ với changes, thuận tiện nhưng loại mô hình phá vỡ).

Một ví dụ đơn giản quen thuộc từ nhiều GUI sẽ là nếu tôi muốn phản ứng với các lần nhấp chuột và có shift-click làm điều gì đó khác với một lần nhấp khi phím shift không được giữ. Với một số Behaviour giữ một giá trị cho biết liệu phím shift có được giữ lại hay không, điều này là không đáng kể. Nếu tôi chỉ có Event s để bấm phím shift/release và cho nhấp chuột thì khó hơn nhiều.

Ngoài việc khó khăn hơn, mức độ thấp hơn nhiều. Tại sao tôi phải làm phức tạp fiddling chỉ để thực hiện một khái niệm đơn giản như shift-click? Sự lựa chọn giữa BehaviourEvent là một cách trừu tượng hữu ích để triển khai các khái niệm của chương trình của bạn dưới dạng bản đồ chặt chẽ hơn với cách bạn nghĩ về chúng bên ngoài thế giới lập trình.

Ví dụ ở đây sẽ là đối tượng di chuyển trong thế giới trò chơi. Tôi có thể có một Event Position đại diện cho tất cả các lần di chuyển. Hoặc tôi chỉ có thể có một Behaviour Position đại diện cho nó ở mọi lúc. Thông thường, tôi sẽ nghĩ về đối tượng là có vị trí ở mọi thời điểm, vì vậy, Behaviour phù hợp hơn với khái niệm.

Địa điểm khác Behaviour s là hữu ích để biểu diễn các quan sát bên ngoài mà chương trình của bạn có thể thực hiện, nơi bạn chỉ có thể kiểm tra giá trị "hiện tại" (vì hệ thống bên ngoài sẽ không thông báo cho bạn khi có thay đổi).

Ví dụ: giả sử chương trình của bạn phải theo dõi cảm biến nhiệt độ và tránh bắt đầu công việc khi nhiệt độ quá cao. Với một số Event Temperature, tôi sẽ quyết định trước tần suất thăm dò ý kiến ​​của cảm biến nhiệt độ (hoặc để đáp ứng với điều gì). Và sau đó có tất cả các vấn đề tương tự như trong các ví dụ khác của tôi về việc phải tự làm điều gì đó để làm cho việc đọc nhiệt độ cuối cùng có sẵn cho sự kiện quyết định có bắt đầu một công việc hay không. Hoặc tôi có thể sử dụng fromPoll để tạo Behaviour Temperature. Bây giờ tôi đã có một giá trị đại diện cho giá trị nhiệt độ thay đổi theo thời gian và tôi đã hoàn toàn trừu tượng hóa việc bỏ phiếu cảm biến; Bản thân Banana phản ứng sẽ quan tâm đến việc bỏ phiếu cảm biến thường xuyên vì nó có thể cần thiết mà không cần tôi phải sắp xếp bất kỳ logic nào cho điều đó!

+0

Bạn có thể xin hãy giải thích về "phản ứng Banana tự chăm sóc bỏ phiếu cảm biến thường xuyên như nó có thể là cần thiết"? Làm thế nào nó có thể biết tần suất một hành động IO cần được thăm dò ý kiến? – arrowd

+0

@arrowdodger Bởi vì mọi thứ chỉ "xảy ra" để đáp ứng với các sự kiện, các giá trị thực tế một hành vi diễn ra giữa các lần xuất hiện các sự kiện phụ thuộc vào nó hoàn toàn không liên quan. Vì vậy, về cơ bản nó đủ để chạy các hành động bỏ phiếu bất cứ khi nào một sự kiện phụ thuộc xảy ra (tôi đã nghe phản ứng chuối thực sự thăm dò ý kiến ​​thường xuyên hơn mặc dù; bất cứ khi nào * bất kỳ * sự kiện đầu vào xảy ra). Vì vậy, về cơ bản nó vẫn khá "giống như sự kiện" dưới mui xe, nhưng không giống như nếu chúng ta mô hình hóa cảm biến nhiệt độ như một sự kiện, chúng ta không phải sửa một chiến lược lấy mẫu khi xác định nó. – Ben

+0

Cảm ơn, đúng như tôi nghĩ ban đầu. – arrowd

7

Behavior s có giá trị tất cả các thời gian, trong khi Event s chỉ có giá trị tại ngay lập tức.

Hãy suy nghĩ về điều đó giống như bạn làm trong bảng tính - hầu hết dữ liệu tồn tại dưới dạng giá trị ổn định (Hành vi) treo xung quanh và được cập nhật bất cứ khi nào cần thiết. (Trong FRP, phụ thuộc có thể đi theo cả hai chiều mà không có vấn đề tham chiếu vòng tròn - dữ liệu được cập nhật từ giá trị đã thay đổi sang giá trị không thay đổi.) Bạn có thể bổ sung thêm mã kích hoạt khi bạn nhấn một nút hoặc làm điều gì đó khác, nhưng hầu hết dữ liệu luôn sẵn sàng. Chắc chắn bạn có thể làm tất cả điều đó chỉ với các sự kiện - khi điều này thay đổi, đọc giá trị này và giá trị đó và đầu ra giá trị này, nhưng nó chỉ rõ ràng hơn để thể hiện những mối quan hệ đó và để cho bảng tính hoặc trình biên dịch lo lắng về thời điểm cập nhật nội dung cho bạn.

stepper là để thay đổi những điều xảy ra thành giá trị trong ô và change là để xem các ô và kích hoạt hành động. Ví dụ của bạn nơi đầu ra là văn bản trên một dòng lệnh không bị ảnh hưởng đặc biệt bởi việc thiếu dữ liệu liên tục, bởi vì đầu ra đi kèm trong các cụm.

Nếu tuy nhiên bạn có giao diện người dùng đồ họa, mô hình chỉ sự kiện, trong khi chắc chắn có thể và thực sự phổ biến, hơi cồng kềnh so với mô hình FRP. Trong FRP bạn chỉ cần xác định mối quan hệ giữa mọi thứ mà không được rõ ràng về cập nhật.

Không phải cần thiết để có Hành vi và tương tự bạn có thể lập trình bảng tính Excel hoàn toàn bằng VBA mà không có công thức. Nó chỉ đẹp hơn với dữ liệu vĩnh cửu và đặc điểm kỹ thuật equational.Khi bạn đã quen với mô hình mới, bạn sẽ không muốn quay trở lại theo cách thủ công theo dõi phụ thuộc và cập nhật nội dung.

3

Khi bạn chỉ có 1 Event, hoặc nhiều sự kiện xảy ra cùng một lúc, hoặc nhiều sự kiện cùng loại, thật dễ dàng để chỉ union hoặc kết hợp chúng thành một sự kiện kết quả, sau đó vượt qua để reactimate và ngay lập tức ra nó. Nhưng nếu bạn có 2 Sự kiện của 2 loại khác nhau diễn ra vào những thời điểm khác nhau thì sao? Sau đó kết hợp chúng thành một sự kiện kết quả mà bạn có thể chuyển đến reactimate trở thành một biến chứng không cần thiết.

Tôi khuyên bạn nên thực sự thử và triển khai bộ tổng hợp từ FRP explanation using reactive-banana chỉ với Sự kiện và không có Hành vi, bạn sẽ nhanh chóng thấy rằng Hành vi đơn giản hóa các thao tác Sự kiện không cần thiết.

Giả sử chúng tôi có 2 Sự kiện, xuất Octave (nhập từ đồng nghĩa cho Int) và Pitch (nhập từ đồng nghĩa vào Char). Dùng nhấn phím từ một để g để thiết lập sân hiện tại, hoặc nhấn + hay - để tăng hoặc giảm quãng tám hiện hành. Chương trình sẽ xuất ra quãng hiện tại và quãng tám hiện tại, như a0, b2 hoặc f7. Hãy nói rằng người dùng nhấn các phím trong các kết hợp khác nhau trong những thời điểm khác nhau, vì vậy chúng tôi đã kết thúc với 2 dòng sự kiện (sự kiện) như thế:

+  -  + -- octave stream (time goes from left to right) 
    b  c  -- pitch stream 

Mỗi người sử dụng thời gian nhấn một phím, chúng tôi ra quãng tám hiện tại và sân. Nhưng điều gì sẽ là sự kiện kết quả? Giả sử độ cao mặc định là a và quãng tám mặc định là 0. Chúng ta nên kết thúc với một dòng sự kiện trông như thế này:

a1 b1 b0 c0 c1 -- a1 corresponds to + event, b1 to b, b0 to -, etc 

đơn giản nhân vật đầu vào/đầu ra

Hãy cố gắng thực hiện tổng hợp từ đầu và xem liệu chúng ta có thể làm điều đó mà không Hành vi. Hãy đầu tiên viết một chương trình, nơi bạn đặt một nhân vật, nhấn Nhập, chương trình đầu ra nó, và yêu cầu một nhân vật nữa:

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) 

Simple sự kiện mạng

Hãy làm các việc trên nhưng với một mạng sự kiện, để minh họa cho họ.

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) 

Mạng là nơi tất cả các sự kiện và hành vi của bạn sống và tương tác với nhau. Họ chỉ có thể làm điều đó bên trong Moment ngữ cảnh đơn sắc. Trong hướng dẫn Functional Reactive Programming kick-starter guide sự tương tự cho mạng sự kiện là bộ não con người. Một bộ não con người là nơi tất cả các luồng sự kiện và hành vi xen kẽ với nhau, nhưng cách duy nhất để truy cập vào bộ não là thông qua các thụ thể, hoạt động như nguồn sự kiện (đầu vào).

Bây giờ, trước khi chúng tôi tiến hành, cẩn thận kiểm tra các loại các chức năng quan trọng nhất của đoạn mã trên:

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() 

Bởi vì chúng tôi sử dụng giao diện người dùng đơn giản nhất có thể - nhân vật đầu vào/đầu ra, chúng ta sẽ sử dụng mô-đun Control.Event.Handler, được cung cấp bởi Phản ứng chuối. Thông thường, thư viện GUI thực hiện công việc dơ bẩn này cho chúng ta.

Chức năng loại Handler chỉ là một hành động IO, tương tự như các hành động IO khác như getChar hoặc putStrLn (ví dụ:cái sau có loại String -> IO()). Một chức năng của loại Handler có một giá trị và thực hiện một số tính toán IO với nó. Do đó, nó chỉ có thể được sử dụng bên trong ngữ cảnh IO (ví dụ: main).

Từ loại đó là rõ ràng (nếu bạn hiểu điều cơ bản của monads) mà fromAddHandlerreactimate chỉ có thể được sử dụng trong Moment ngữ cảnh (ví dụ makeDescriptionNetwork), trong khi newAddHandler, compileactuate chỉ có thể được sử dụng trong IO ngữ cảnh (ví dụ main).

Bạn tạo một cặp giá trị của các loại AddHandlerHandler sử dụng newAddHandler trong main, bạn vượt qua AddHandler chức năng mới này đến chức năng sự kiện mạng của bạn, nơi bạn có thể tạo ra một dòng sự kiện ra khỏi nó bằng cách sử fromAddHandler. Bạn thao tác luồng sự kiện này nhiều như bạn muốn, sau đó bọc các sự kiện của nó trong một hành động IO và chuyển luồng sự kiện kết quả thành reactimate.

lọc sự kiện

Bây giờ chúng ta hãy chỉ ra điều gì đó, nếu người dùng nhấn + hay -. Hãy đầu ra 1 khi người dùng nhấn +, -1 khi người dùng nhấn -. (Phần còn lại của mã vẫn giữ nguyên).

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' 

Như chúng ta làm không ra nếu người dùng nhấn bất cứ điều gì bên cạnh + hay -, cách tiếp cận sạch sẽ là:

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' 

chức năng quan trọng cho thao tác tổ chức sự kiện (xem Reactive.Banana.Combinators để biết thêm) :

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 

Tăng dần và giảm tích lũy s

Nhưng chúng tôi không muốn chỉ đầu ra 1 và -1, chúng tôi muốn tăng và giảm giá trị và ghi nhớ nó giữa các lần nhấn phím! Vì vậy, chúng ta cần phải accumE. accumE chấp nhận giá trị và luồng các chức năng thuộc loại (a -> a). Mỗi lần hàm mới xuất hiện từ luồng này, hàm này được áp dụng cho giá trị và kết quả được ghi nhớ. Lần tới khi hàm mới xuất hiện, nó được áp dụng cho giá trị mới, v.v. Điều này cho phép chúng ta nhớ, con số nào chúng ta hiện đang có để giảm hoặc tăng.

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 về cơ bản là một dòng của các chức năng (+1), (-1), (+1), tùy thuộc vào chính người dùng nhấn.

Hợp nhất hai luồng sự kiện

Bây giờ chúng tôi sẵn sàng triển khai cả quãng tám và quảng cáo chiêu hàng từ bài viết gốc.

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 

Chương trình của chúng tôi sẽ xuất ra quãng hiện tại hoặc quãng tám hiện tại, tùy thuộc vào những gì người dùng nhấn. Nó cũng sẽ bảo tồn giá trị của quãng tám hiện tại. Nhưng chờ đã! Đó không phải là những gì chúng tôi muốn!Điều gì sẽ xảy ra nếu chúng tôi muốn xuất cả quãng hiện tại và quãng tám hiện tại, mỗi khi người dùng nhấn một chữ cái hoặc + hoặc -?

Và ở đây nó trở nên siêu cứng. Chúng tôi không thể kết hợp 2 loại sự kiện khác nhau, vì vậy chúng tôi có thể chuyển đổi cả hai loại sự kiện thành Event t (Pitch, Octave). Nhưng nếu sự kiện quảng cáo chiêu hàng và sự kiện quãng tám xảy ra vào thời điểm khác nhau (tức là chúng không đồng thời, thực tế nhất định trong ví dụ của chúng tôi), thì luồng sự kiện tạm thời của chúng tôi sẽ có loại Event t (Maybe Pitch, Maybe Octave), với Nothing ở mọi nơi bạn không tương ứng biến cố. Vì vậy, nếu người dùng nhấn theo thứ tự + b - c + và chúng tôi giả định rằng quãng tám mặc định là 0 và sân mặc định là a, thì chúng tôi kết thúc bằng một chuỗi các cặp [(Nothing, Just 1), (Just 'b', Nothing), (Nothing, Just 0), (Just 'c', Nothing), (Nothing, Just 1)], được bọc trong Event.

Sau đó, chúng ta phải tìm ra cách thay thế Nothing với những gì sẽ là cường độ hiện tại hoặc quãng tám, do đó chuỗi kết quả phải giống như [('a', 1), ('b', 1), ('b', 0), ('c', 0), ('c', 1)].

Đây là cấp độ quá thấp và một lập trình viên thực sự không nên lo lắng về việc sắp xếp các sự kiện như vậy, khi có bản tóm tắt cấp cao khả dụng.

Hành vi đơn giản hóa thao tác sự kiện

Một vài sửa đổi đơn giản và chúng tôi đã đạt được kết quả tương tự.

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 

Bật sân tổ chức sự kiện vào Behavior với stepper và thay thế accumE với accumB để có được hành vi quãng tám thay vì quãng tám tổ chức sự kiện. Để có được Hành vi kết quả, hãy sử dụng applicative style.

Sau đó, để nhận sự kiện, bạn phải chuyển đến reactimate, chuyển Hành vi kết quả đến changes. Tuy nhiên, changes trả lại một giá trị monadic phức tạp Moment t (Event t (Future a)), do đó bạn nên sử dụng reactimate' thay vì reactimate. Đây cũng là lý do, tại sao bạn phải nhấc putStrLn vào ví dụ trên hai lần vào eResult, vì bạn đang nâng nó lên Future functor bên trong Event functor.

Kiểm tra các loại chức năng, chúng tôi sử dụng ở đây để hiểu những gì diễn ra ở đâu:

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() 
Các vấn đề liên quan