23

在经典 FRP 的最新实现中,例如反应香蕉,有事件流和信号,它们是阶跃函数(反应香蕉称它们为行为,但它们仍然是阶跃函数)。我注意到 Elm 只使用信号,并没有区分信号和事件流。此外,reactive-banana 允许从事件流到信号(已编辑:虽然它不被认为是好的做法,但可以使用 reactimate 对行为采取行动),这意味着理论上我们可以应用所有事件流通过首先将信号转换为事件流,应用然后再次转换来对信号/行为进行组合器。所以,考虑到它通常更容易使用和学习一种抽象,分离信号和事件流有什么好处?仅使用信号并将所有事件流组合器转换为对信号进行操作是否会丢失任何东西?

编辑:讨论非常有趣。我自己从讨论中得出的主要结论是,相互递归定义(反馈)和输出依赖于两个输入(一个行为和一个事件源)都需要行为/事件源,但只有在一个输入时才会导致动作其中有变化 (<@>)。

4

4 回答 4

24

(澄清:在 reactive-banana 中,不可能将Behaviorback 转换为Event。该stepper函数是单程票。有一个changes函数,但它的类型表明它是“不纯的”,并带有警告说它不保留语义。)

我相信拥有两个独立的概念会使 API 更加优雅。换句话说,它归结为 API 可用性的问题。我认为这两个概念的行为完全不同,如果你有两种不同的类型,事情就会变得更好。

例如,每种类型的直接产品是不同的。一对 Behavior 等价于一对 Behavior

(Behavior a, Behavior b) ~ Behavior (a,b)

而一对事件等价于直接的事件:

(Event    a, Event    b) ~ Event (EitherOrBoth a b)

如果将这两种类型合并为一种,那么这些等价性将不再成立。

然而,将事件和行为分离的主要原因之一是后者没有更改“更新”的概念。起初这似乎是一个遗漏,但在实践中它非常有用,因为它导致代码更简单。例如,考虑newInput创建一个输入 GUI 小部件的一元函数,该小部件显示参数 Behavior 中指示的文本,

input <- newInput (bText :: Behavior String)

现在的关键点是,显示的文本取决于Behavior的更新频率bText(相同或不同的值),而仅取决于实际值本身。这比另一种情况更容易推理,在这种情况下,您必须考虑当两个连续事件发生具有相同值时会发生什么。您是否在用户编辑文本时重绘文本?

(当然,为了实际绘制文本,库必须与 GUI 框架交互,并跟踪 Behavior 的变化。这就是changes组合器的用途。然而,这可以看作是一种优化,是不能从“FRP 内”获得。)

分离的另一个主要原因是递归。大多数递归地依赖于自身的事件是不明确的。但是,如果事件和行为之间存在相互递归,则始终允许递归

e = ((+) <$> b) <@> einput
b = stepper 0 e

无需手动引入延迟,它开箱即用。

于 2014-04-11T13:01:32.507 回答
16

对我来说至关重要的东西丢失了,即行为的本质,​​它是(可能是连续的)随连续时间变化的。精确、简单、有用的语义(独立于特定的实现或执行)也经常丢失。查看我对“功能响应式编程语言规范”的回答,并点击那里的链接。

无论是在时间上还是在空间上,过早的离散化都会阻碍可组合性并使语义复杂化。考虑矢量图形(和其他空间连续模型,如Pan的)。就像为什么函数式编程很重要中所解释的数据结构的过早限定一样。

于 2014-04-14T01:58:15.997 回答
4

我认为使用信号/行为抽象而不是 elm 风格的信号没有任何好处。正如您所指出的,可以在信号/行为 API 之上创建一个仅信号 API(还没有准备好使用,但请参阅https://github.com/JohnLato/impulse/blob/dyn2/src/以Reactive/Impulse/Syntax2.hs为例)。我很确定也可以在 elm 风格的 API 之上编写信号/行为 API。这将使这两个 API 在功能上等效。

WRT 效率,使用仅信号 API,系统应该具有一种机制,其中只有具有更新值的信号才会导致重新计算(例如,如果您不移动鼠标,FRP 网络将不会重新计算指针坐标并重绘屏幕)。如果这样做了,我认为与信号和流方法相比,不会有任何效率损失。我很确定 Elm 是这样工作的。

我不认为连续行为问题在这里(或根本没有)有任何区别。人们所说的行为是随时间连续的,是指它们在任何时候都是被定义的(即它们是连续域上的函数);行为本身不是连续函数。但我们实际上并没有办法随时对行为进行采样;它们只能在对应于事件的时间被采样,所以我们不能使用这个定义的全部力量!

从语义上讲,从这些定义开始:

Event    == for some t ∈ T: [(t,a)]
Behavior == ∀ t ∈ T: t -> b

由于只能在定义事件的时间对行为进行采样,因此我们可以创建一个新域TX,其中是定义事件TX的所有时间的集合t。现在我们可以将 Behavior 定义放宽为

Behavior == ∀ t ∈ TX: t -> b

不失去任何权力(即,这相当于我们 frp 系统范围内的原始定义)。现在我们可以枚举所有时间以TX将其转换为

Behavior == ∀ t ∈ TX: [(t,b)]

除了域和量化外,它与原始Event定义相同。现在我们可以改变Eventto的定义域TX(通过 的定义TX),以及Behavior(从 forall 到 for some)的量化,我们得到

Event    == for some t ∈ TX: [(t,a)]
Behavior == for some t ∈ TX: [(t,b)]

现在EventBehavior在语义上是相同的,因此它们显然可以在 FRP 系统中使用相同的结构来表示。在这一步我们确实丢失了一些信息;如果我们不区分Event并且Behavior我们不知道每次Behavior定义了 a ,但实际上我认为这并不重要。IIRC 的 elm 要求s 和s 在任何时候都具有值,并且如果它没有改变,则只使用先前的值(即改变 to 的量化而不是改变 的量化tEventBehaviorEventEventforallBehavior)。这意味着您可以将所有内容视为一个信号,并且一切正常;它只是实现了,因此信号域正是系统实际使用的时间子集。

我认为这个想法是在一篇关于在 Java 中实现 FRP 的论文中提出的(我现在找不到,还有其他人有链接吗?),也许来自 POPL '14?凭记忆工作,所以我的大纲不如原始证明那么严格。

没有什么可以阻止您创建Behavior由 eg定义的更多内容pure someFunction,这仅意味着在 FRP 系统中您无法使用该额外定义性,因此更受限制的实现不会丢失任何内容。

至于时间等名义信号,请注意,使用典型的编程语言不可能实现实际的连续时间信号。由于实现必然是离散的,因此将其转换为事件流是微不足道的。

简而言之,我认为仅使用信号不会丢失任何东西。

于 2014-04-10T21:56:32.480 回答
1

不幸的是,我没有参考,但我清楚地记得不同的反应式作者声称这种选择只是为了提高效率。您将两者都公开以让程序员选择相同想法的哪种实现更有效地解决您的问题。

我现在可能在撒谎,但我相信 Elm 将所有东西都作为事件流实现。不过,时间之类的东西不会像事件流那么好,因为在任何时间范围内都有无限数量的事件。我不确定 Elm 是如何解决这个问题的,但我认为这是一个很好的例子,它作为一个信号更有意义,无论是在概念上还是在实现上。

于 2014-04-10T18:36:16.590 回答