11

我正在学习反应香蕉。为了理解这个库,我决定实现一个虚拟应用程序,只要有人按下按钮,它就会增加一个计数器。

我使用的 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

这只是将结果转储到 shell 中。我在阅读Heinrich Apfelmus的文章时提出了这个解决方案。请注意,在我的示例中,我没有使用单个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

该示例显示 a将 astepper转换Event为 aBehavior并带回Eventusing changes。在上面的例子中,我们可以只使用Event它,我猜它不会有任何区别(除非我不理解某些东西)。

那么有人可以阐明何时使用Behavior以及为什么使用?Event我们应该尽快转换所有s 吗?

在我的小实验中,我看不到Behavior可以在哪里使用。

谢谢

4

3 回答 3

7

每当 FRP 网络在 Reactive Banana 中“做某事”时,这是因为它正在对某些输入事件做出反应。它在系统之外做任何可观察的事情的唯一方法是连接一个外部系统来响应它生成的事件(使用reactimate)。

因此,如果您所做的只是通过生成输出事件来立即对输入事件做出反应,那么不,您将找不到太多使用Behaviour.

Behaviour对于产生依赖于多个事件流的程序行为非常有用,您必须记住事件发生在不同的时间

Event出现;它具有价值的特定时刻。ABehaviour在所有时间点都有一个值,没有特殊的时刻(除了changes,它很方便但有点破坏模型)。

许多 GUI 都熟悉的一个简单示例是,如果我想对鼠标单击做出反应,并在不按住 shift 键时让 shift-click 执行与单击不同的操作。使用Behaviour一个值来指示是否按住 shift 键,这是微不足道的。如果我只有Events 用于 shift 键按下/释放和鼠标点击,那就更难了。

除了更难之外,它的级别也低得多。为什么我必须做复杂的摆弄才能实现像 shift-click 这样的简单概念?和之间的选择是一个有用的抽象,可以更接近地映射到您在编程世界之外思考它们的方式来实现您的程序概念。BehaviourEvent

这里的一个例子是游戏世界中的可移动对象。我可以有一个Event Position代表它移动的所有时间。或者我可以一直Behaviour Position代表它在哪里。通常我会认为对象总是一个位置,所以Behaviour是一个更好的概念契合。

另一个Behaviour有用的地方是表示程序可以进行的外部观察,您只能检查“当前”值(因为外部系统不会在发生更改时通知您)。

例如,假设您的程序必须密切关注温度传感器,并避免在温度过高时开始工作。有了一个,Event Temperature我将预先决定多久轮询一次温度传感器(或响应什么)。然后与我的其他示例中的所有问题相同,即必须手动执行某些操作以使最后的温度读数可用于决定是否开始工作的事件。或者我可以fromPoll用来制作一个Behaviour Temperature. 现在我得到了一个代表温度随时间变化的值,并且我已经完全从轮询传感器中抽象出来了;Reactive Banana 本身负责尽可能频繁地轮询传感器,而我根本不需要为此添加任何逻辑!

于 2014-11-07T23:32:27.643 回答
7

Behaviors 始终有一个值,而Events 只在瞬间有一个值。

像在电子表格中一样思考它 - 大多数数据以稳定值(行为)的形式存在,并在必要时随时更新。(尽管在 FRP 中,依赖关系可以采用任何一种方式而不会出现循环引用问题 - 数据会从已更改的值流向未更改的值。)您还可以添加在按下按钮或执行其他操作时触发的代码,但大多数情况下数据随时可用。

当然,您可以只使用事件来完成所有这些工作 - 当它发生变化时,读取这个值和那个值并输出这个值,但是以声明方式表达这些关系并让电子表格或编译器担心何时为您更新内容会更简洁。

stepper用于将发生的事情更改为单元格中的值,change用于观察单元格并触发动作。您的输出是命令行上的文本的示例并没有受到缺乏持久数据的特别影响,因为无论如何输出都是突发的。

但是,如果您有一个图形用户界面,那么仅事件模型虽然肯定是可能的,而且确实很常见,但与 FRP 模型相比有点麻烦。在 FRP 中,您只需指定事物之间的关系,而无需明确更新。

不一定要有行为,类似地,您可以完全在 VBA 中编写一个没有公式的 Excel 电子表格数据持久性和等式规范更好。一旦你习惯了新的范式,你就不想回到手动追踪依赖项和更新东西了。

于 2014-11-06T23:09:08.537 回答
3

当您只有 1 个Event或多个同时发生的事件或多个相同类型的事件时,很容易union将它们组合成一个结果事件,然后传递给reactimate并立即输出它。但是,如果您有 2 种不同类型的 2 种事件在不同时间发生怎么办?然后将它们组合成一个结果事件,您可以传递给它reactimate成为不必要的复杂性。

我建议您实际尝试并使用只有事件而没有行为的反应香蕉来实现 FRP 解释中的合成器,您会很快看到行为简化了不必要的事件操作。

假设我们有 2 个事件,输出 Octave(类型为 Int 的同义词)和 Pitch(类型为 Char 的同义词)。用户按a至键g设置当前音高,或按+-增加或减少当前八度。程序应输出当前音高和当前八度音程,如a0b2f7。假设用户在不同时间以各种组合按下这些键,所以我们最终得到了 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单子上下文中做到这一点。在教程函数响应式编程启动指南中,事件网络的类比是人脑。人脑是所有事件流和行为相互交错的地方,但访问大脑的唯一方法是通过充当事件源(输入)的受体。

现在,在我们继续之前,请仔细检查上述代码片段中最重要的函数的类型:

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

因为我们使用了最简单的 UI——字符输入/输出,所以我们将使用Control.Event.HandlerReactive-banana 提供的模块。通常,GUI 库会为我们完成这项肮脏的工作。

type 的函数Handler只是一个 IO 动作,类似于其他 IO 动作,例如getCharor putStrLn(例如后者有 type String -> IO ())。一个类型的函数Handler接受一个值并用它执行一些 IO 计算。因此它只能在 IO 上下文中使用(例如 in main)。

从类型很明显(如果您了解 monad 的基础知识),fromAddHandlerandreactimate只能在Moment上下文中使用(例如makeDescriptionNetwork),而newAddHandler, compileandactuate只能在IO上下文中使用(例如main)。

您创建一对类型值AddHandlerHandler使用newAddHandlerin main,将此新AddHandler函数传递给您的事件网络函数,您可以在其中使用fromAddHandler. 您可以随心所欲地操作此事件流,然后将其事件包装在一个 IO 操作中,并将生成的事件流传递给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'

+如果用户按下or旁边的任何内容,我们不会输出-,因此更简洁的方法是:

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

累积增量和减量

但是我们不想只输出 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

我们的程序将输出当前音高或当前八度音程,具体取决于用户按下的内容。它还将保留当前八度的值。可是等等!这不是我们想要的!如果我们想输出当前音高和当前八度音阶,每次用户按下一个字母+或时-怎么办?

在这里它变得超级难。我们不能合并 2 个不同类型的事件流,所以我们可以将它们都转换为Event t (Pitch, Octave). 但是如果一个音高事件和一个八度事件发生在不同的时间(即它们不是同时发生的,这在我们的例子中实际上是确定的),那么我们的临时事件流宁愿有 type Event t (Maybe Pitch, Maybe Octave),而Nothing在任何地方你都没有相应的事件。因此,如果用户按顺序按下+ b - c +,并且我们假设默认八度为 0,默认音高为 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并替换accumEaccumB以获得八度音程行为而不是八度音程事件。要获得结果行为,请使用applicative style

然后,要获取必须传递给 的事件reactimate,请将生成的 Behavior 传递给changes。但是,changes返回一个复杂的一元值Moment t (Event t (Future a)),因此您应该使用reactimate'而不是reactimate. 这也是为什么你必须putStrLn在上面的例子中提升两次 into的eResult原因,因为你将它提升到Futurefunctor 内的Eventfunctor。

查看我们在此处使用的函数的类型,以了解其中的内容:

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 ()
于 2015-07-24T12:22:13.717 回答