Когда у вас есть только 1 Событие или несколько Событий, которые происходят одновременно, или несколько Событий одного типа, легко просто union
или иным образом объединить их в результирующее событие, а затем передать reactimate
и сразу выводить. Но что, если у вас есть 2 события 2 разных типов, происходящих в разное время? Затем объединение их в результирующее событие, которое вы можете передать reactimate
, становится ненужным усложнением.
Я рекомендую вам попробовать реализовать синтезатор из объяснения FRP с использованием реактивного банана< /a> только с Событиями и без Поведений, вы быстро увидите, что Поведения упрощают ненужные манипуляции с Событиями.
Скажем, у нас есть 2 события, выводящие Octave (тип, синоним Int) и Pitch (тип, синоним Char). Пользователь нажимает клавиши от a до g, чтобы установить текущий шаг, или нажимает + или -, чтобы увеличить или уменьшить текущий октава. Программа должна выводить текущую высоту тона и текущую октаву, например a0
, b2
или f7
. Допустим, пользователь нажимал эти клавиши в разных комбинациях в разное время, в итоге у нас получилось 2 потока событий (Events) вот так:
+ - + -- 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
Простой ввод/вывод символов
Давайте попробуем реализовать синтезатор с нуля и посмотрим, сможем ли мы сделать это без поведения. Давайте сначала напишем программу, где вы вводите символ, нажимаете Enter, программа выводит его и снова запрашивает символ:
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
монадический контекст. В учебнике Functional Reactive Programming kick- начальное руководство аналогией сети событий является человеческий мозг. Человеческий мозг — это место, где все потоки событий и поведение перемежаются друг с другом, но единственный способ получить доступ к мозгу — через рецепторы, которые действуют как источник событий (вход).
Теперь, прежде чем мы продолжим, внимательно проверьте типы наиболее важных функций приведенного выше фрагмента:
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
— это просто действие ввода-вывода, аналогичное другим действиям ввода-вывода, таким как getChar
или putStrLn
(например, последнее имеет тип String -> IO ()
). Функция типа Handler
принимает значение и выполняет с ним некоторые вычисления ввода-вывода. Таким образом, его можно использовать только внутри контекста ввода-вывода (например, в main
).
Из типов очевидно (если вы понимаете основы монад), что fromAddHandler
и reactimate
можно использовать только в контексте Moment
(например, makeDescriptionNetwork
), а newAddHandler
, compile
и actuate
можно использовать только в контексте IO
(например, main
).
Вы создаете пару значений типов AddHandler
и Handler
, используя newAddHandler
в main
, вы передаете эту новую функцию AddHandler
своей функции сети событий, где вы можете создать из нее поток событий, используя fromAddHandler
. Вы манипулируете этим потоком событий столько, сколько хотите, затем оборачиваете его события в действие ввода-вывода и передаете результирующий поток событий в reactimate
.
Фильтрация событий
Теперь давайте выводить что-то, только если пользователь нажимает + или -. Давайте выведем 1, когда пользователь нажимает +, -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
< /a> для получения дополнительной информации):
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
Накопление инкрементов и декрементов
Но мы не хотим просто выводить 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
Наша программа будет выводить либо текущую высоту тона, либо текущую октаву, в зависимости от того, что нажал пользователь. Это также сохранит значение текущей октавы. Но ждать! Это не то, что мы хотим! Что, если мы хотим выводить как текущую высоту тона, так и текущую октаву каждый раз, когда пользователь нажимает либо букву, либо +, либо -?
И здесь это становится сверхсложным. Мы не можем объединить 2 потока событий разных типов, поэтому мы можем преобразовать их оба в 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
и замените accumE
на accumB
, чтобы получить октавное поведение вместо октавного события. Чтобы получить результирующее поведение, используйте аппликативный стиль.
Затем, чтобы получить событие, которое вы должны передать reactimate
, передайте полученное поведение в changes
. Однако changes
возвращает сложное монадическое значение Moment t (Event t (Future a))
, поэтому следует использовать reactimate'
вместо reactimate
. Это также является причиной того, что вы должны поднять putStrLn
в приведенном выше примере дважды до eResult
, потому что вы поднимаете его до функтора Future
внутри функтора Event
.
Ознакомьтесь с типами функций, которые мы использовали здесь, чтобы понять, что и где происходит:
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 ()
person
Mirzhan Irkegulov
schedule
24.07.2015