来自反应式香蕉行为的动态输入

Dynamic inputs from a Behavior in reactive-banana

当我想在 reactive-banana 中使用动态输入时,我通常会设想一个大致如下工作的系统:

data InputSpecification -- Some data structure that specifies the inputs
                        -- which should be active right now

data MyInterestingData -- Some data that is relevant to my business logic
                       -- and is gathered through the dynamic inputs.

emptyData :: MyInterestingData
emptyData = -- Some initial MyInterestingData

setupDynamicInputs :: Event (InputSpecification) -> MomentIO (Behavior MyInterestingData)
setupDynamicInputs specE = do
    newBehavior <- execute $ updateDynamicInputs <$> specE
    switchB emptyData newBehavior

updateDynamicInputs :: InputSpecification -> MomentIO (Behavior MyInterestingData)
updateDynamicInputs = -- Here the dynamic inputs are set up according to
                      -- the specification and set up to update the returned
                      -- Behavior

这工作得很好,只要触发新的 InputSpecification,输入就会更新。

我经常遇到的一个问题是我的 InputSpecification 不是 Event 而是 Behavior InputSpecification (可能是因为我需要 Applicative 组合器构建它)。上面的方法不起作用,因为 executeswitchB 不能用于 Behaviors.

作为一个简单的解决方案,我可以使用 reactive-banana documentation:

中的这个函数
plainChanges :: Behavior a -> MomentIO (Event a)
plainChanges b = do
    (e, handle) <- newEvent
    eb <- changes b
    reactimate' $ (fmap handle) <$> eb
    return e

然后我可以在从 plainChanges 获得的 Event 上使用 setupDynamicInputs,但是:

However, this approach is not recommended[...]

所以我有点不愿意使用这种方法。

当规范保存在 Behavior 中时,是否有 "cleaner" 方法使我的输入与规范保持同步?

编辑

正如 Heinrich Apfelmus 在他的回答中指出的那样,我最初问题的解决方案是不使用 Behavior 来更新 InputSpecification。虽然我能理解这背后的原因,但它并不能解决我遇到的问题,所以我会尝试解释为什么我想在这里使用 Behavior

只要输入由单个输入指定,通过 Event 更新输入很容易。例如,如果动态输入由一系列输入组成,那么这些输入的规范将只是一个非负整数,表示应显示多少输入。

一旦通过多个输入获得输入规范,它就会变得更加复杂。例如,假设我们的 InputSpecification 变为 (Word, Word) 并指定具有给定维度的输入网格。如果我通过两个不同的输入获得这些维度,我将不得不将两个 Event Word 组合成一个 Event (Word, Word),这对于 Event 来说并不是一个微不足道的任务,因为它们不有一个像 Behavior 那样的 Applicative 实例。这就是为什么我通常喜欢在这种情况下使用 Behaviors,但正如之前所讨论的,当您真正想要创建输入时,它们不会让您更进一步。因此,如果 Behavior 在这里不是正确的解决方案并且 Event 往往很难(或者在最坏的情况下不可能)结合起来,那么这个问题的正确解决方案是什么?

好吧,这可能不起作用的原因之一是它也许不应该起作用。有一个试金石可以确定用 Behavior 对情况建模是否有意义:如果 Behavior InputSpecification 是一个真正的 连续 函数会怎样?比如说,您有一个连续的频率范围(例如,对于无线电台),每个频率都将关联到一个必须设置的新输入。如果要进行连续频率扫描,则必须创建和丢弃无限多的输入,这是不可能的。这表明 Event InputSpecification 是正确的类型背后有更深层次的原因。

更一般地说,Behavior 类型封装了两个重要的不变量:

  1. 它不依赖于采样率。
  2. 您无法检测到它何时或多久出现一次"changes"。例如,如果你有一个 Event,它的出现都具有相同的值 [(0 seconds, x), (2 seconds, x), ..],那么这个不变量表示对它应用 stepper 将产生一个 Behavior,即 无法区分pure x.

出于实用的原因,可以使用 changes 函数规避不变量 2。觉得可以就可以用"morally preserves the invariant"。例如,您可以使用它仅在行为发生变化时在屏幕上显示文本值;这比以固定采样率轮询更有效。由于上述任一行为的视觉最终结果相同,因此在这种情况下您在道义上保留了不变量。


编辑:

您似乎需要更明确地控制何时更新。在这种情况下,您可以使用显式事件 e :: Event () 来跟踪何时应更新输入。然后,您可以使用以下组合仅在该事件触发时更新输入

e2 <- plainChanges (imposeChanges b e)
execute $ updateDynamicInputs <$> e2
...

(应该有一个纯粹的替代方案,我会研究这个。)

或者,您可以手动复制 "Behavior with notification for updates" 机器,例如引入类型

data Dynamic a = D (Behavior a) (Event a)

并为此实施 Applicative 等实例。这有点重量级,但可能正是您所需要的。