在 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 中应用行为(和其他类型)的位置的主要内容,如果未能解决你的问题,请参考以下文章