9

在之前的 SO 问题(Is it possible?: Behavior t [Behavior ta] -> Behavior t [a])中,我们正在分析 a 的存在Behavior join(使用reactive-banana术语)。

Behavior t (Behavior t a) -> Behavior t a

在语义模型中实现如下

type Behavior t a = t -> a

behaviorNow :: Behavior t (Behavior t a) -> Behavior t a
behaviorNow f t = f t t

虽然直接实现这将是不幸的,因为我们可以产生一个Behavior Monadusing constand behaviorNow,如果以及如何behaviorNow违反 FRP 的语义?

我很想听到使用任何其他 FRP 系统的术语以及有意义的比较的答案。

4

2 回答 2

7

在基于投票的 FRP 系统中,任何行为都有有意义的join

  • 的样本join bb是抽样b得到的样本bb

在基于推送的 FRP 中,任何作为与其他阶跃函数组合的阶跃函数的行为都具有有意义的>>=join。推送值>>=可以用命令式术语来描述:

  • 当绑定的参数发生变化时,评估绑定并
    • 将当前阶跃函数更改为返回的阶跃函数
    • 将值更改为当前阶跃函数的值
  • 当当前阶跃函数的值改变时,改变值

提供一个Monad实例可能有点不受欢迎,因为图书馆用户可能会根据偏好选择它,即使它的效率较低。例如,这个不相关的答案中的代码在使用 构建计算时执行的工作>>=比使用<*>.

Conal Elliott 用声明性术语描述了join同时推送和轮询从阶跃函数构建的行为的值:

-- Reactive is a behavior that can only be a step function
data Reactive a = a `Stepper` Event a
newtype Event a = Ev (Future (Reactive a))

join :: Reactive (Reactive a) -> Reactive a
join ((a `Stepper` Ev ur) `Stepper` Ev urr ) =
    ((`switcher` Ev urr ) <$> ur) _+_ (join <$> urr )

switcher :: Reactive a -> Event (Reactive a) -> Reactive a
r `switcher` er = join (r `Stepper` er)

whereFuture是我们尚未看到的值的类型, _+_是两种Future可能性中的第一种,并且是s 的<$>中缀fmap[1]Future

如果我们不提供任何其他创建行为的方法

  • 常数函数(它实际上是一个阶跃函数)
  • 记住事件的最新值的“步进器”
  • 应用各种行为组合器,其中组合器本身不是随时间变化的

那么每个行为都是一个阶跃函数,我们可以使用这个或类似Monad的行为实例。

只有当我们想要有连续的行为或者是某个时间的函数而不是事件发生时,才会出现困难。考虑我们是否有以下

time :: Behavior t t

这是跟踪当前时间的行为。轮询系统的Monad实例仍然是相同的,但我们不能再可靠地通过系统推送更改。当我们做一些简单的事情时会发生什么time >>= \x -> if am x then return 0 else return 1am t早上的时间是真的)?我们上面的定义>>=和 Elliot 的定义都join不能承认知道时间何时变化的优化;它不断变化。我们能做的最好>>=的事情是:

  • 如果我们知道绑定的参数是步值,那么
    • 当绑定的参数发生变化时,评估绑定并
      • 将当前阶跃函数更改为返回的阶跃函数
      • 将值更改为当前阶跃函数的值
    • 当当前阶跃函数的值改变时,改变值
  • 否则
    • 为此返回一个抽象语法树>>=

对于join表单,我们将简化为做类似的事情,并在 a 中的外部行为join不是阶跃函数的情况下简单地记录 AST。

此外,使用它作为输入构建的任何东西都可能在中午和午夜发生变化,无论是否引发了任何其他事件。从那时起,它会污染一切,无法可靠地推送事件。

从实现的角度来看,我们最好的选择似乎是不断地轮询time,并用从轮询事件构建的步进器替换它使用的任何地方。这不会在事件之间更新值,所以现在我们库的用户不能可靠地轮询值。

我们最好的实现选择是保留一个抽象的语法树,记录这些任意行为所发生的事情,并且不提供从行为生成事件的方法。然后可以对行为进行轮询,但不会为它们推送任何更新。在这种情况下,我们不妨将其从库中排除,让用户传递 AST(他们可以得到Free),并让用户在每次轮询时评估整个 AST。我们不能再为图书馆用户优化它,因为像这样的任何值都可以不断变化。

有一个最后的选择,但它涉及引入相当多的复杂性。介绍连续变化值的属性和连续变化值的计算的可预测性概念。这将允许我们为更大的时变行为子集提供 Monad 接口,但不是为所有行为。这种复杂性在程序的其他部分已经是可取的,但我不知道符号数学之外的任何库试图解决这个问题。

于 2014-01-02T22:54:35.143 回答
6

(作者在这里。)

首先请注意,该behaviorNow函数是 monadic join

在 reactive-banana-0.7 中,Behavior t不是一个会对效率产生严重后果的单子。

第一个也是最重要的问题是行为也可以是有状态的。再加上join,这会导致时间泄漏。问题的主要迹象是内部的开始时间 与外部的相同。例如,考虑程序tBehavior t

e  :: Event t Int
b  :: Int -> Behavior t Int
b x = accumB 0 $ (x+) <$ e

bb :: Behavior t (Behavior t Int)
bb = stepper (pure 0) $ b <$> e

行为join bb将需要跟踪事件的整个历史,e以便在 的定义中执行累积b。换句话说,事件e永远不会被垃圾收集——时间泄漏。

第二个问题是,在内部,实现Behavior t还包括一个跟踪行为何时发生变化的事件。然而,自由使用join组合子,例如do符号所暗示的,将导致相当复杂的计算来确定行为是否发生了变化。这与首先​​跟踪的原因相反:通过避免昂贵的计算来提高效率。


Reactive.Banana.Switch模块提供了各种组合器,它们是函数的表亲,join但巧妙地选择类型可以避免第一个问题。尤其是:

  • switchB函数是 的最直接类似物join
  • 类型与AnyMoment Identity类型相似Behavior,但没有状态并且不跟踪更改。因此,它有一个 monad 实例。
于 2014-01-03T10:20:10.197 回答