在最近的Classic FRP实现中,例如reactive-banana,有事件流和信号,它们是阶梯函数(reactive-banana称它们为行为,但它们仍然是阶梯函数)。我注意到Elm只使用信号,并没有区分信号和事件流。此外,反应性香蕉允许从事件流转变为信号(编辑:并且它可以使用重新作用对行为采取行动'虽然它不被认为是良好做法),这种意味着理论上我们通过首先将信号转换为事件流,应用然后再次转换,可以将所有事件流组合器应用于信号/行为。因此,鉴于它通常更容易使用并且只学习一个抽象,分离信号和事件流的优势是什么?在使用信号和转换所有事件流组合器以对信号进行操作时是否有任何损失?
编辑:讨论非常有趣。我自己讨论的主要结论是,行为/事件源既需要相互递归的定义(反馈),也需要输出依赖于两个输入(一个行为和一个事件源),但只在一个时产生一个动作它们的变化(< @>)。
答案 0 :(得分:23)
(澄清:在反应性香蕉中,无法将Behavior
转换回Event
。stepper
函数是单程票。有一个{ {1}}函数,但它的类型表明它是“不纯的”,它带有一个警告,它不保留语义。)
我相信有两个分离概念使API更优雅。换句话说,它归结为API可用性的问题。我认为这两个概念的行为完全不同,如果你有两种不同的类型,事情会更好地流动。
例如,每种类型的直接产品都不同。一对行为等同于行的行为
changes
而一对事件相当于直接总和的事件:
(Behavior a, Behavior b) ~ Behavior (a,b)
如果将两种类型合并为一种,那么这些等价物都不会再存在。
然而,事件和行为分离的主要原因之一是后者不有更改或“更新”的概念。这看起来似乎是一个遗漏,但它在实践中非常有用,因为它会导致更简单的代码。例如,考虑一个monadic函数(Event a, Event b) ~ Event (EitherOrBoth a b)
,它创建一个输入GUI小部件,显示参数Behavior中指示的文本,
newInput
现在的关键点是显示的文字不取决于 行为input <- newInput (bText :: Behavior String)
可能更新的频率(相同或不同)值),仅限于实际值本身。这比其他情况更容易推理,在这种情况下,您必须考虑当两个连续的事件发生具有相同值时会发生什么。在用户编辑文本时是否重绘文本?
(当然,为了实际绘制文本,库必须与GUI框架接口并跟踪行为的变化。这就是bText
组合器的用途。但是,这可以看作是一种优化,不能从“FRP内”获得。)
分离的另一个主要原因是递归。递归依赖于自身的大多数事件都是不明确的。但是,如果事件和行为之间存在相互递归,则递归始终
changes
无需手动引入延迟,只需开箱即用。
答案 1 :(得分:14)
对我来说至关重要的东西已经失去,即行为的本质,即连续时间的(可能是连续的)变化。 精确,简单,有用的语义(独立于特定的实现或执行)通常也会丢失。 查看my answer到“功能反应式编程语言规范”,并点击那里的链接。
无论是在时间上还是在太空中,过早的离散化都会阻碍可组合性并使语义复杂化。 考虑矢量图形(以及其他空间连续模型,如Pan)。正如Why Functional Programming Matters中所述的数据结构的过早确定一样。
答案 2 :(得分:4)
我认为使用信号/行为抽象而不是榆树式信号有任何好处。正如您所指出的,可以在信号/行为API之上创建一个仅信号API(完全没有准备好使用,但请参阅https://github.com/JohnLato/impulse/blob/dyn2/src/Reactive/Impulse/Syntax2.hs示例)。我很确定也可以在elm风格的API之上编写信号/行为API。这将使两个API功能相同。
WRT效率,使用仅信号API,系统应该有一种机制,其中只有具有更新值的信号才会导致重新计算(例如,如果不移动鼠标,FRP网络将不会重新计算指针坐标并重绘屏幕)。如果这样做,我认为与信号和流方法相比,效率没有任何损失。我很确定榆树是这样工作的。
我认为持续行为问题在这里(或者根本没有)会产生任何不同。人们所说的行为随着时间的推移是连续的意思是它们始终被定义(即它们是连续域上的函数);行为本身不是一个连续的功能。但我们实际上并没有办法随时对行为进行抽样;它们只能在与事件相对应的时间进行采样,因此我们无法使用此定义的全部功能!
从语义上讲,从这些定义开始:
Event == for some t ∈ T: [(t,a)]
Behavior == ∀ t ∈ T: t -> b
由于行为只能在定义事件时进行采样,因此我们可以创建一个新域TX
,其中TX
是定义事件的所有时间t
的集合。现在我们可以将行为定义放宽到
Behavior == ∀ t ∈ TX: t -> b
不会失去任何力量(即这相当于我们frp系统范围内的原始定义)。现在我们可以在TX
中枚举所有时间以将其转换为
Behavior == ∀ t ∈ TX: [(t,b)]
除了域和量化之外,与原始Event
定义相同。现在我们可以将Event
的域更改为TX
(通过TX
的定义),以及Behavior
的量化(从forall到某些),我们得到< / p>
Event == for some t ∈ TX: [(t,a)]
Behavior == for some t ∈ TX: [(t,b)]
现在Event
和Behavior
在语义上是相同的,因此显然可以在FRP系统中使用相同的结构来表示它们。我们确实在这一步失去了一些信息;如果我们不区分Event
和Behavior
,我们不知道每个时间Behavior
定义t
,但是练习我不认为这真的很重要。 IIRC的榆树需要Event
和Behavior
s始终具有值,如果Event
没有改变,只需使用前一个值(即更改量化) Event
到forall
而不是更改Behavior
的量化。这意味着你可以将所有东西都当作一个信号对待,而这一切都是Just Works它刚刚实现,因此信号域正是系统实际使用的时间子集。
我认为这个想法是在一篇论文中提出的(我现在找不到,其他人都有链接?)关于在Java中实现FRP,或许来自POPL '14?在记忆中工作,所以我的轮廓不像原始证明那么严格。
没有什么可以阻止您通过例如创建更明确的Behavior
pure someFunction
,这只是意味着在FRP系统中你不能利用那个额外的定义,所以没有什么因为更受限制的实现而丢失。
对于时间等概念信号,请注意,使用典型的编程语言无法实现实际的连续时间信号。由于实现必然是离散的,因此将其转换为事件流是微不足道的。
简而言之,我认为只使用信号就不会丢失任何东西。
答案 3 :(得分:1)
我遗憾地没有提及任何参考,但我清楚地记得 不同的反应作者声称这种选择只是为了提高效率。揭露 两者都可以让程序员选择相同想法的实现 更有效地解决您的问题。
我现在可能在撒谎,但我相信Elm会将所有内容都视为事件流 引擎盖。像时间这样的事情不会像事件流那么好,因为有一个 在任何时间范围内无限量的事件。我不确定Elm是如何解决这个问题的,但我 从概念上讲,这是一个很好的例子,它可以作为一种信号更有意义 并在实施中。