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à fromAddHandler
và reactimate
chỉ có thể được sử dụng trong Moment
ngữ cảnh (ví dụ makeDescriptionNetwork
), trong khi newAddHandler
, compile
và actuate
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 AddHandler
và Handler
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()
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
@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
Cảm ơn, đúng như tôi nghĩ ban đầu. – arrowd