我们为什么要在FRP中使用Behavior

时间:2014-11-06 16:54:54

标签: haskell reactive-programming reactive-banana

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

我使用的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的article解决方案。请注意,在我的示例中,我没有使用单个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

示例显示stepperEvent转换为Behavior并使用Event带回changes。在上面的例子中,我们只能使用Event,我猜它没有任何区别(除非我不理解某些东西)。

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

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

由于

3 个答案:

答案 0 :(得分:7)

Behavior一直有一个值,而Event只有一个值。

将其视为电子表格中的大部分内容 - 大多数数据都以稳定值(行为)的形式存在,并在必要时进行更新。 (在FRP中,依赖关系可以在没有循环引用问题的情况下进行 - 数据更新从更改的值更新为未更改的值。)您还可以添加按下按钮或执行其他操作时触发的代码,但大多数情况下数据一直可用。

当然,你可以只用事件来做所有这些 - 当这个改变时,读取这个值和那个值并输出这个值,但是以声明的方式表达这些关系并让电子表格或编译器担心什么时候会更加清晰为你更新东西。

stepper用于更改发生在单元格中的值的内容,change用于监视单元格和触发操作。输出是命令行上的文本的示例并不特别受缺少持久数据的影响,因为无论如何输出都是突发的。

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

拥有行为并非必要,类似地,您可以完全在VBA中编写Excel电子表格而不使用公式。它具有数据持久性和等式规范的优点。一旦你习惯了新的范例,你就不想回到手动追逐依赖关系和更新东西了。

答案 1 :(得分:6)

任何时候FRP网络在Reactive Banana中“做某事”都是因为它对某些输入事件做出了反应。在系统外部进行任何可观察的事情的唯一方法是连接外部系统以对其生成的事件作出反应(使用reactimate)。

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

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

Event已发生;具有价值的特定时间点。 Behaviour在所有时间点都有一个值,没有特定时间的时刻(changes除外,这很方便,但有点模型破坏)。

许多GUI中熟悉的一个简单示例是,如果我想对鼠标点击做出反应,并且在不保持shift键的情况下按住Shift键单击与点击不同的操作。如果Behaviour持有一个值,指示是否按住shift键,这是微不足道的。如果我只有Event用于按键/释放键和鼠标点击它会更难。

除了更难,它还是更低级别。为什么我要做一个复杂的摆弄只是为了实现像shift-click这样的简单概念? BehaviourEvent之间的选择是一个有用的抽象,用于实现您的程序概念,更贴近您在编程世界之外对它们的思考方式。

这里的一个例子是游戏世界中的可移动物体。我可以Event Position表示它移动的所有时间。或者我可以随时使用Behaviour Position表示它在哪里。通常我会将对象视为始终具有位置,因此Behaviour更符合概念。

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

举个例子,假设您的程序必须密切关注温度传感器,并避免在温度过高时启动作业。使用Event Temperature我会事先决定轮询温度传感器的频率(或响应什么)。然后在我的其他示例中遇到所有相同的问题,即必须手动执行某些操作才能使最后一个温度读数可用于决定是否启动作业的事件。或者我可以使用fromPoll制作Behaviour Temperature。现在我有一个代表温度随时间变化的值的值,而且我已完全从轮询传感器中抽象出来; Reactive Banana本身负责按照可能需要的频率轮询传感器,而我根本不需要任何逻辑!

答案 2 :(得分:3)

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

我建议您实际尝试使用FRP explanation using reactive-banana仅使用事件和无行为来实现合成器,您很快就会发现行为简化了不必要的事件操作。

假设我们有2个事件,输出Octave(Int的同义词类型)和Pitch(类型为Char的同义词)。用户按键从 a g 设置当前音高,或者按 + - 增加或减少当前音高八度。该程序应输出当前音高和当前八度音,例如a0b2f7。让我们说用户在不同时间以各种组合方式按下这些键,因此我们最终得到了2个事件流(事件):

   +     -     +   -- 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 monadic上下文中执行此操作。在教程Functional Reactive Programming kick-starter guide中,事件网络的类比是人脑。人脑是所有事件流和行为相互交错的地方,但进入大脑的唯一方法是通过受体,它充当事件源(输入)。

现在,在我们继续之前,请仔细查看上述代码段最重要功能的类型:

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 - 字符输入/输出,我们将使用Reactive-banana提供的模块Control.Event.Handler。通常,GUI库为我们做这个肮脏的工作。

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

从类型中可以明显看出(如果您了解monad的基础知识)fromAddHandlerreactimate只能在Moment上下文中使用(例如makeDescriptionNetwork) ,newAddHandlercompileactuate只能在IO上下文中使用(例如main)。

您使用AddHandler中的Handler创建了一对newAddHandlermain类型的值,您将这个新的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'

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

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

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

这里变得非常难熬。我们无法合并两个不同类型的事件流,因此我们可以将它们转换为Event t (Pitch, Octave)。但是如果音高事件和八度音阶事件发生在不同的时间(即它们不是同时发生的,这在我们的示例中几乎是确定的),那么我们的临时事件流宁愿具有类型Event t (Maybe Pitch, Maybe Octave),{{1}你到处都是相应的事件。因此,如果用户按顺序按 + b - c + ,并且我们假设默认八度为0且默认音高为Nothing,那么我们最终得到一对{{1} },包裹在a

然后我们必须弄清楚如何用当前音高或八度音程替换[(Nothing, Just 1), (Just 'b', Nothing), (Nothing, Just 0), (Just 'c', Nothing), (Nothing, Just 1)],所以结果序列应该是Event

这太低级了,当有高级抽象可用时,真正的程序员不应该担心这样的事件的对齐。

行为简化了事件操作

一些简单的修改,我们取得了相同的结果。

Nothing

使用stepper将音调事件转换为行为,并将[('a', 1), ('b', 1), ('b', 0), ('c', 0), ('c', 1)]替换为accumB以获得八度行为而不是八度音频事件。要获得最终的行为,请使用applicative style

然后,要获取该事件,您必须传递给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 ,将生成的行为传递给changes。但是,accumE会返回复杂的monadic值reactimate,因此您应该使用reactimate'而不是changes。这也是为什么你必须将上面的例子中的Moment t (Event t (Future a))两次提升到reactimate的原因,因为你要将它提升到putStrLn仿函数内的eResult仿函数

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

Future