FRP - 事件流和信号 - 使用信号时会丢失什么?

mig*_*rao 22 haskell functional-programming frp reactive-banana elm

在Classic FRP的最近实现中,例如反应性香蕉,存在事件流和信号,它们是阶梯函数(反应性香蕉称它们为行为,但它们仍然是阶梯函数).我注意到Elm只使用信号,并没有区分信号和事件流.此外,反应性香蕉允许从事件流转变为信号(编辑:并且它可以使用重新作用对行为采取行动'虽然不被认为是良好做法),这意味着理论上我们可以应用所有事件流通过首先将信号转换为事件流,应用然后再次转换,对信号/行为进行组合.因此,鉴于它通常更容易使用并只学习一个抽象,分离信号和事件流的优势是什么?在使用信号和转换所有事件流组合器以对信号进行操作时是否有任何损失?

编辑:讨论非常有趣.我自己讨论的主要结论是,行为/事件源既需要相互递归的定义(反馈),也需要输出依赖于两个输入(一个行为和一个事件源),但只在一个时产生一个动作.他们改变了(<@>).

Hei*_*mus 23

(澄清:在反应性香蕉中,不可能将一个Behavior回转换为Event.该stepper功能是单程票.有一个changes功能,但它的类型表明它是"不纯的"并且它附带一个警告,它不保留语义.)

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

例如,每种类型的直接产品是不同的.一对行为相当于一对行为

(Behavior a, Behavior b) ~ Behavior (a,b)
Run Code Online (Sandbox Code Playgroud)

而一对事件相当于直接的事件:

(Event    a, Event    b) ~ Event (EitherOrBoth a b)
Run Code Online (Sandbox Code Playgroud)

如果将两种类型合并为一种,那么这些等价都不会再存在.

然而,主要的原因的事件和行为的分离之一是,后者并没有有一个概念的变化或"更新".这看起来似乎是一个遗漏,但它在实践中非常有用,因为它会导致更简单的代码.例如,考虑一个monadic函数newInput,它创建一个输入GUI小部件,显示参数Behavior中指示的文本,

input <- newInput (bText :: Behavior String)
Run Code Online (Sandbox Code Playgroud)

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

(当然,为了实际绘制文本,库必须与GUI框架接口并跟踪行为的变化.这就是changes组合器的用途.但是,这可以看作是一种优化,并且不在"FRP内".)

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

e = ((+) <$> b) <@> einput
b = stepper 0 e
Run Code Online (Sandbox Code Playgroud)

没有必要手工引入延迟,它只是开箱即用.


Con*_*nal 14

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

无论是在时间上还是在太空中,过早的离散化都会阻碍可组合性并使语义复杂化.考虑矢量图形(和的其他空间连续模型).正如"功能编程为何重要"中所解释的那样,数据结构的过早完成也是如此.


Joh*_*n L 5

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

\n\n

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

\n\n

我认为连续行为问题在这里没有任何影响(或者根本没有影响)。人们所说的行为随着时间的推移是连续的,意思是它们在任何时候都是被定义的(即它们是连续域上的函数);行为本身不是连续函数。但我们实际上没有办法随时对行为进行采样;它们只能在与事件相对应的时间进行采样,因此我们无法使用此定义的全部功能!

\n\n

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

\n\n
Event    == for some t \xe2\x88\x88 T: [(t,a)]\nBehavior == \xe2\x88\x80 t \xe2\x88\x88 T: t -> b\n
Run Code Online (Sandbox Code Playgroud)\n\n

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

\n\n
Behavior == \xe2\x88\x80 t \xe2\x88\x88 TX: t -> b\n
Run Code Online (Sandbox Code Playgroud)\n\n

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

\n\n
Behavior == \xe2\x88\x80 t \xe2\x88\x88 TX: [(t,b)]\n
Run Code Online (Sandbox Code Playgroud)\n\n

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

\n\n
Event    == for some t \xe2\x88\x88 TX: [(t,a)]\nBehavior == for some t \xe2\x88\x88 TX: [(t,b)]\n
Run Code Online (Sandbox Code Playgroud)\n\n

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

\n\n

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

\n\n

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

\n\n

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

\n\n

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

\n

  • 老实说,人们“确实”意味着行为是随着时间的推移而连续的函数。例如,您可以将移动的事物描述为其坐标的行为。你可以得到它的导数(随着时间的推移),并对其速度有一个行为。当然,您是对的,我们只能在离散时间步长上对这种行为进行采样,但它仍然被定义为能够在任意时刻产生不同的值。 (2认同)
  • 此外,带箭头的 frp 系统在语义上往往是完全连续的,甚至用 Maybes 的信号函数表示事件,尽管其中一些又是基于推送的,所以在事件之间没有发生任何事情 (Yampa)。当然,在推送系统上,您仍然可以使用计时器以固定速率迭代时间。 (2认同)