我正在使用reactive-banana开发一个程序,我想知道如何使用基本的FRP构建块来构建我的类型。
例如,这是我真实程序的一个简化示例:假设我的系统主要由小部件组成 - 在我的程序中,随时间变化的文本片段。
我可以
newtype Widget = Widget { widgetText :: Behavior String }
但我也可以
newtype Widget = Widget { widgetText :: String }
并在我想谈论时变行为时使用Behavior Widget
。这似乎使事情“更简单”,并且意味着我可以更直接地使用Behavior
操作,而不是必须解压缩和重新打包Widgets来执行此操作。
另一方面,前者似乎避免了实际定义小部件的代码中的重复,因为几乎所有的小部件都会随着时间的推移而变化,我发现自己甚至定义了少数没有Behavior
的小部件。因为它允许我以更一致的方式将它们与其他人结合起来。
作为另一个例子,使用两种表示形式,有一个Monoid
实例(我希望在我的程序中有一个)是有意义的,但后者的实现似乎更自然(因为它只是一个微不足道的将列表monoid提升为newtype)。
(我的实际计划使用Discrete
而不是Behavior
,但我认为这不相关。)
同样,我应该使用Behavior (Coord,Coord)
或(Behavior Coord, Behavior Coord)
来表示2D点吗?在这种情况下,前者似乎是明显的选择;但是当它是一个五元素的记录,表示游戏中的某个实体时,选择似乎不那么明确。
从本质上讲,所有这些问题都归结为:
使用FRP时,我应该在哪个图层应用Behavior
类型?
(同样的问题也适用于Event
,尽管程度较轻。)
答案 0 :(得分:6)
我在开发FRP应用程序时使用的规则是:
Behavior
/ Event
。(1)的原因是,如果您使用的数据类型尽可能原始,则创建和组合抽象操作会变得更容易。
原因是Monoid
之类的实例可以重复用于原始类型,如您所述。
请注意,您可以使用Lenses轻松修改数据类型的“内容”,就像它们是原始值一样,因此额外的“包装/解包”不是问题。 (有关此特定镜头实现的介绍,请参阅this recent tutorial;有others)
(2)的原因是它只是消除了不必要的开销。如果两件事同时发生变化,那么它们“具有相同的行为”,因此应该对它们进行建模。
Ergo / tl; dr :由于(1),你应该使用newtype Widget = Widget { widgetText :: Behavior String }
,因为(2)你应该使用Behavior (Coord, Coord)
(因为两个坐标通常都会改变)同时地)。
答案 1 :(得分:5)
Behavior/Event
。并且想为这些经验法则提供其他理由。
问题可归结为以下几点:您想表示一对(元组)值随时间变化,问题是是否使用
一个。 (Behavior x, Behavior y)
- 一对行为
湾Behavior (x,y)
- 对的行为
优先选择其中一个的原因是
a over b 。
在推送驱动的实现中,行为的更改将触发重新计算依赖于它的所有行为。
现在,考虑一种行为,其价值仅取决于货币对的第一个组成部分x
。在变体 a 中,第二个组件y
的更改不会重新计算该行为。但是在变体 b 中,行为将被重新计算,即使它的值根本不依赖于第二个组件。换句话说,这是一个细粒度与粗粒度依赖关系的问题。
这是建议的论据1.当然,当两种行为趋于同时改变时,这并不重要,这会产生建议2。
当然,即使对于 b 变种,库也应该提供一种提供细粒度依赖关系的方法。至于反应性香蕉版本0.4.3,这是不可能的,但是现在不用担心,我的推动式实现在未来版本中将会成熟。
b通过。
看到反应性香蕉版本0.4.3尚未提供dynamic event switching,但是如果将所有组件放在一个行为中,则只能编写某些程序。标准示例将是具有变量数量的计数器的程序,即TwoCounter.hs示例的扩展。您必须将其表示为时间变化值列表
counters :: Behavior [Int]
因为无法跟踪动态的行为集合。也就是说,反应香蕉的下一个版本将包括动态事件切换。
此外,您始终可以毫不费力地将变种 a 转换为变种 b
uncurry (liftA2 (,)) :: (Behavior a, Behavior b) -> Behavior (a,b)