Почему мы должны использовать поведение в FRP

Я изучаю реактивный банан. Чтобы понять библиотеку, я решил реализовать фиктивное приложение, которое будет увеличивать счетчик всякий раз, когда кто-то нажимает кнопку.

Я использую библиотеку пользовательского интерфейса 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

Это просто сбрасывает результат в оболочку. Я пришел к этому решению, прочитав статью Генриха Апфельмуса. Обратите внимание, что в моем примере я не использовал ни одного 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, который преобразует Event в Behavior и возвращает Event с помощью changes. В приведенном выше примере мы могли бы использовать только Event, и я думаю, что это не имело бы никакого значения (если только я чего-то не понимаю).

Так может ли кто-нибудь пролить свет на то, когда использовать Behavior и почему? Должны ли мы преобразовать все Event как можно скорее?

В моем небольшом эксперименте я не вижу, где можно использовать Behavior.

Спасибо


person mathk    schedule 06.11.2014    source источник


Ответы (3)


Каждый раз, когда сеть FRP «что-то делает» в Reactive Banana, это происходит потому, что она реагирует на какое-то входное событие. И единственный способ, которым он делает что-либо наблюдаемое вне системы, — это подключение внешней системы для реагирования на генерируемые ею события (используя reactimate).

Итак, если все, что вы делаете, это немедленно реагировать на входное событие, создавая выходное событие, то нет, вы не найдете особых причин для использования Behaviour.

Behaviour очень полезен для создания поведения программы, которое зависит от нескольких потоков событий, когда вы должны помнить, что события происходят в разное время.

Event имеет вхождения; конкретные моменты времени, когда она имеет значение. Behaviour имеет значение во все моменты времени, без каких-либо особенных моментов времени (за исключением changes, что удобно, но нарушает модель).

Простой пример, знакомый по многим графическим интерфейсам, был бы, если бы я хотел реагировать на щелчки мыши и чтобы Shift-щелчок делал что-то отличное от щелчка, когда клавиша Shift не удерживается. Когда Behaviour содержит значение, указывающее, удерживается ли нажатой клавиша Shift, это тривиально. Если бы у меня было только Events для нажатия/отпускания клавиши Shift и для щелчков мышью, это было бы намного сложнее.

Помимо того, что это сложнее, это гораздо более низкий уровень. Почему я должен делать сложные возни только для того, чтобы реализовать простую концепцию, такую ​​​​как Shift-щелчок? Выбор между Behaviour и Event — полезная абстракция для реализации концепций вашей программы в терминах, которые более точно соответствуют тому, как вы думаете о них за пределами мира программирования.

Примером здесь может быть подвижный объект в игровом мире. Я мог бы иметь Event Position, представляющее все времена, когда он перемещается. Или я мог бы просто иметь Behaviour Position, представляющий, где он находится в любое время. Обычно я буду думать об объекте как о имеющем положении все время, поэтому Behaviour лучше подходит концептуально.

Еще одно место, где Behaviour полезны, - это представление внешних наблюдений, которые может сделать ваша программа, где вы можете проверить только «текущее» значение (поскольку внешняя система не будет уведомлять вас, когда происходят изменения).

Например, предположим, что ваша программа должна следить за датчиком температуры и избегать запуска задания при слишком высокой температуре. С Event Temperature мне придется заранее решить, как часто опрашивать датчик температуры (или в ответ на что). И затем возникают все те же проблемы, что и в других моих примерах, о необходимости что-то делать вручную, чтобы сделать последнее показание температуры доступным для события, которое решает, запускать задание или нет. Или я мог бы использовать fromPoll, чтобы сделать Behaviour Temperature. Теперь у меня есть значение, представляющее изменяющееся во времени значение температуры, и я полностью абстрагировался от опроса датчика; Reactive Banana сама заботится об опросе датчика так часто, как это может понадобиться, и мне вообще не нужно придумывать для этого какую-либо логику!

person Ben    schedule 07.11.2014
comment
Не могли бы вы уточнить, что Reactive Banana сама заботится об опросе датчика так часто, как это может понадобиться? Откуда ему знать, как часто нужно опрашивать действие ввода-вывода? - person arrowd; 04.05.2015
comment
@arrowdodger Поскольку вещи происходят только в ответ на события, фактические значения, которые поведение принимает между возникновением событий, которые зависят от него, совершенно не имеют значения. Таким образом, по сути, достаточно запускать действие опроса всякий раз, когда происходит зависимое событие (я слышал, что Reactive Banana на самом деле опрашивает чаще, чем это; всякий раз, когда происходит любое событие ввода). Таким образом, под капотом все еще очень похоже на событие, но в отличие от того, если бы мы смоделировали датчик температуры как событие, нам не нужно исправлять стратегию выборки, когда мы ее определяем. - person Ben; 04.05.2015
comment
Спасибо, как я и думал изначально. - person arrowd; 04.05.2015

Behaviors имеют значение все время, тогда как Events имеют значение только в данный момент.

Думайте об этом так же, как об электронной таблице — большая часть данных существует в виде стабильных значений (поведений), которые висят вокруг и обновляются по мере необходимости. (Однако в FRP зависимость может идти в любом направлении без проблем с циклическими ссылками — данные обновляются, переходя от измененного значения к неизмененному.) Вы можете дополнительно добавить код, который срабатывает, когда вы нажимаете кнопку или делаете что-то еще, но большинство данные доступны все время.

Конечно, вы могли бы делать все это только с событиями - когда это изменяется, считывайте это значение и то значение и выводите это значение, но проще выразить эти отношения декларативно и позволить электронной таблице или компилятору беспокоиться о том, когда обновлять материал для вас.

stepper предназначен для преобразования событий в значения в ячейках, а change — для наблюдения за ячейками и запуска действий. На ваш пример, где вывод представляет собой текст в командной строке, не особенно влияет отсутствие постоянных данных, потому что вывод в любом случае происходит в виде пакетов.

Однако, если у вас есть графический пользовательский интерфейс, модель только событий, хотя, безусловно, возможна и действительно распространена, немного громоздка по сравнению с моделью FRP. В FRP вы просто указываете отношения между вещами, не говоря об обновлениях явно.

необязательно иметь поведение, и аналогичным образом вы можете запрограммировать электронную таблицу Excel полностью на VBA без формул. Это просто лучше с постоянством данных и спецификацией уравнений. Как только вы привыкнете к новой парадигме, вам не захочется возвращаться к ручному поиску зависимостей и обновлению материала.

person AndrewC    schedule 06.11.2014

Когда у вас есть только 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