在 FRP 中应用行为(和其他类型)的位置

Posted

技术标签:

【中文标题】在 FRP 中应用行为(和其他类型)的位置【英文标题】:Where to apply Behavior (and other types) in FRP 【发布时间】:2012-01-25 12:13:24 【问题描述】:

我正在使用reactive-banana 开发一个程序,我想知道如何使用基本的 FRP 构建块来构建我的类型。

例如,这是我真实程序中的一个简化示例:假设我的系统主要由 widgets 组成——在我的程序中,是随时间变化的文本片段。

我本来可以

newtype Widget = Widget  widgetText :: Behavior String 

但我也可以拥有

newtype Widget = Widget  widgetText :: String 

当我想谈论随时间变化的行为时,请使用Behavior Widget。这似乎让事情变得“更简单”,并且意味着我可以更直接地使用Behavior 操作,而不必解包和重新打包小部件来执行此操作。

另一方面,前者似乎避免了实际定义小部件的代码中的重复,因为几乎所有小部件都随时间而变化,我发现自己甚至定义了少数不使用 Behavior 的小部件,因为它让我能够以更一致的方式将它们与其他人结合起来。

作为另一个例子,对于这两种表示,拥有一个 Monoid 实例是有意义的(我想在我的程序中拥有一个),但后者的实现似乎更自然(因为它只是一个微不足道的提升列表幺半群到新类型)。

(我的实际程序使用Discrete 而不是Behavior,但我认为这无关紧要。)

同样,我应该使用Behavior (Coord,Coord) 还是(Behavior Coord, Behavior Coord) 来表示二维点?在这种情况下,前者似乎是显而易见的选择;但当它是代表游戏中实体之类的五元素记录时,选择似乎不太明确。

本质上,所有这些问题都归结为:

使用FRP时,应该在哪一层应用Behavior类型?

(同样的问题也适用于Event,尽管程度较轻。)

【问题讨论】:

【参考方案1】:

我同意dflemstr's advice到

    尽可能隔离“变化的事物”。 将“同时发生变化的事物”归为一个 Behavior/Event

并想就这些经验法则提供其他理由。

问题归结为:你想表示一对(元组)随时间变化的值,问题是是否使用

一个。 (Behavior x, Behavior y) - 一对行为

b. Behavior (x,y) - 成对行为

选择其中一个的原因是

a 大于 b.

在推送驱动的实现中,行为的改变会触发所有依赖它的行为的重新计算。

现在,考虑一个行为,其值仅取决于该对的第一个组件 x。在变体 a 中,第二个组件 y 的更改不会重新计算行为。但是在变体 b 中,行为将被重新计算,即使它的值根本不依赖于第二个组件。换句话说,这是一个细粒度与粗粒度依赖关系的问题。

这是建议 1 的一个论据。当然,当两种行为倾向于同时改变,从而产生建议 2 时,这并不重要。

当然,库应该提供一种方法来提供细粒度的依赖关系,即使对于变体 b。从响应式香蕉版本 0.4.3 开始,这是不可能的,但暂时不要担心,我的推送驱动实现将在未来版本中成熟。

b 大于 a

看到 reactive-banana 版本 0.4.3 还没有提供dynamic event switching,有些程序只有在你将所有组件放在一个行为中时才能编写。规范示例是具有 可变 个计数器的程序,即 TwoCounter.hs 示例的扩展。您必须将其表示为随时间变化的值列表

counters :: Behavior [Int]

因为目前还没有办法跟踪动态的行为集合。也就是说,reactive-banana 的下一个版本将包括动态事件切换。

此外,您可以随时从变体 a 转换为变体 b 而不会有任何麻烦

uncurry (liftA2 (,)) :: (Behavior a, Behavior b) -> Behavior (a,b)

【讨论】:

嗯,在Widget的情况下,只有一个字段不是简化,这是我的实际情况,所以不涉及元组:) 不过谢谢你的帮助——应该非常对以后有帮助!我现在将Behavior 放在新类型中。我希望我能接受这两个答案:)【参考方案2】:

我在开发 FRP 应用程序时使用的规则是:

    尽可能隔离“变化的事物”。 将“同时变化的事物”归为一个Behavior/Event

(1) 的原因是,如果您使用的数据类型尽可能原始,则创建和组合抽象操作会变得更容易。

原因是Monoid 之类的实例可以重用于原始类型,正如您所描述的。

请注意,您可以使用Lenses 轻松修改数据类型的“内容”,就好像它们是原始值一样,因此额外的“包装/解包”基本上不是问题。 (有关此特定 Lens 实现的介绍,请参阅 this recent tutorial;有 others)

(2) 的原因是它只是消除了不必要的开销。如果两件事同时发生变化,则它们“具有相同的行为”,因此应该对它们进行建模。

Ergo/tl;dr:由于 (1),您应该使用 newtype Widget = Widget widgetText :: Behavior String ,由于 (2),您应该使用 Behavior (Coord, Coord)(因为两个坐标通常同时变化)。

【讨论】:

我不认为镜头在这里有帮助 — 以 Monoid 为例,就像 f = liftA2 mappend 之类的东西变成了 f a b = Widget $ mappend (widgetText a) (widgetText b)。不可否认,提升组合器可以缓解这种痛苦。但是,我不确定您通过引用我的 Monoid 示例要表达什么 — 它是 String 表单的参数,而不是 Behavior String 表单的参数。 不过,您的规则听起来很不错,我得再考虑一下。非常感谢您发布这个!我暂时不会接受这个答案,因为我想听听其他观点和观点,而且这是一个非常微妙的问题。 在您的Widget 示例中,它仅包含widgetText,这使得原始提升变得微不足道。如果您在 Widget 中有更多值,那么原始提升比通过镜头提升 Behavior 并以这种方式对其执行操作要复杂得多。 啊,你的意思是liftL2 :: Lens a b -> (b -> b -> b) -> a -> a -> a? (嗯,我想你也许可以把这个提升模式变成一个应用函子。) 但这有一个明显的问题:您将结果值放回哪个参数?我认为像这样在多个值上使用镜头是一种误用。

以上是关于在 FRP 中应用行为(和其他类型)的位置的主要内容,如果未能解决你的问题,请参考以下文章

Haskell:FRP 反应性 Parsec?

相同的数据类型,不同的数据行为

控制pod在节点位置与其他类型的pod

FRP第一篇之FRP介绍和基本使用

FRP第一篇之FRP介绍和基本使用

2019-2020-1学期 20192422 《网络空间安全专业导论》第四周学习总结