FRP - 事件流和信号 - 仅使用信号会丢失啥?

Posted

技术标签:

【中文标题】FRP - 事件流和信号 - 仅使用信号会丢失啥?【英文标题】:FRP - Event streams and Signals - what is lost in using just signals?FRP - 事件流和信号 - 仅使用信号会丢失什么? 【发布时间】:2014-05-24 05:33:18 【问题描述】:

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

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

【问题讨论】:

嗯,据我了解,行为是不是阶跃函数,而是随时间连续? conal.net/papers/push-pull-frp 可能是一本好书 是的,行为不是阶跃函数,但据我了解,reactive-banana 所称的行为实际上是阶跃函数,elm 和 reactive-web 称其为信号。谢谢你的链接,我已经读过那篇论文了。 @Bergi 我认为理论上它们应该是连续的,但在实际实现中很难实现真正的连续性,这就是它们被这样实现的原因。 @miguel.negrao 我认为另一个主要结论是行为是瞬时的,而事件可以累积,即支持更新而不是总是依赖于当前值。 【参考方案1】:

很遗憾,我没有想到任何参考资料,但我清楚地记得 不同的反应式作者声称这种选择只是为了提高效率。你暴露 两者都是为了让程序员选择相同想法的实现方式 更有效地解决您的问题。

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

【讨论】:

【参考方案2】:

我认为使用信号/行为抽象而不是 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 定义相同。现在我们可以将Event的域更改为TX(由TX的定义),以及Behavior的量化(从forall到for some),我们得到

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

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

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

没有什么可以阻止你创建一个更明确的Behavior,例如pure someFunction,这只是意味着在 FRP 系统中,您无法利用这种额外的定义性,因此更受限制的实现不会丢失任何内容。

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

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

【讨论】:

Afaik,人们确实意味着随着时间的推移,行为是连续的函数。例如,您会将移动的事物描述为其坐标的行为。您可以获得它的导数(随着时间的推移),并对其速度有一个行为。当然你是对的,我们只能在离散的时间步长上对这样的行为进行采样,但它仍然被定义为能够在任意时刻产生不同的值。 感谢您的详细解答。这是我怀疑的。但实际上,我认为在具有内在时间的基于拉的 FRP 系统中,行为确实不同于阶跃函数,因为它们可以在事件发生之间重新计算,例如通过取决于时间行为(即物理模拟)。我认为行为和信号之间的区别只有在使用纯粹基于推送的系统时才会消失,因为在事件发生之间没有任何事情发生。 此外,带箭头的 frp 系统在语义上往往是完全连续的,甚至用 Maybes 的信号函数表示事件,尽管其中一些再次基于推送,因此在发生之间没有任何反应(Yampa) .当然,在推送系统上,您仍然可以使用计时器以固定速率迭代时间。 抱歉数学上不准确。我的意思是它们是piecewise 连续的,而不是你的回答似乎暗示的piecewise constant。你说得对,因为我们只关心 TX 域,所以用与事件相同的方式来表示它们就足够了。只有 - 正如你所说 - 我们正在失去关于它们连续定义的语义信息。而且可能效率很高,所以[(t, t->b)]会更好。 “TX 是定义事件的所有时间 t 的集合”。我想知道是什么让你认为 TX 不是所有的连续时间。也许您是说,对于特定实现的特定执行,只进行有限多个时间采样。这些采样时间与(独立于执行和实现的)语义无关。【参考方案3】:

(澄清:在反应香蕉中,不可能将Behavior 转换回Eventstepper 函数是单程票。有一个changes 函数,但它的type 表示它是“不纯的”并且它带有一个警告,它不保留语义。)

我相信拥有两个独立的概念会使 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)

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

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

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

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

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

【讨论】:

你的回答很有启发性。只有一件事让我感到困扰。 我发现想要对一个行为做出反应是很平常的事情,因为一个行为可以将几个行为统一为一个(例如(行为a,行为b)->行为(a,b) )。例如,如果您有一个带有 x 坐标的行为和另一个带有 y 坐标的行为,您可能希望将它们合并为一个点,然后在该点上重新执行,以便在更新 x 或 y 时发生更新。这似乎是一件很基本的事情。在我看来,这很奇怪,它不是反应香蕉中的正常操作(必须使用更改并重新反应')。我错过了什么? 另一件事,我认为 elm 没有办法说您希望通过 Applicative 函子(提升或 我已经更新了我的答案以触及“对行为做出反应”这一点。基本上,这个想法是行为不包括变化的概念,所以你永远不会对它们做出反应(至少在 FRP 内部)。获取 Behavior 值的唯一方法是 apply 组合器。这是一件好事,因为它使您免于推理 Behavior 已更新但其值仍然相同的情况的麻烦。 如果你真的想以类似行为的方式合并事件,那么你必须自己动手。这里有一个例子:来自threepenny-guiTidings type。【参考方案4】:

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

无论是在时间上还是在空间上,过早的离散化都会阻碍可组合性并使语义复杂化。 考虑矢量图形(以及其他空间连续模型,如Pan's)。就像Why Functional Programming Matters 中解释的数据结构的过早限定一样。

【讨论】:

顺便说一句,连续时间是成长为 FRP 的单一基本思想(如Early inspirations and new directions in functional reactive programming 中所述),应用了指称语义的优雅和严谨。

以上是关于FRP - 事件流和信号 - 仅使用信号会丢失啥?的主要内容,如果未能解决你的问题,请参考以下文章

进程信号

进程信号

函数响应式编程的“信号”表示是不是正确?

C未从定时器接收到信号

iOS学习笔记39-ReactiveCocoa入门

iOS开发 ReactiveCocoa入门教程 第二部分