为什么我的数据类型需要成为Monoid的实例才能使用这个镜头?

10

我在使用下面的代码处理一个包含类型为SceneGraph的'_scene'字段的记录。我已经使用makeLenses创建了它的镜头。

inputGame :: Input -> Game -> Game
inputGame i g = flip execState g $ do
    let es = g ^. userInput . events
        sg = g ^. scene
    userInput .= i
    scene .= foldl (flip inputEvent) sg es

inputEvent :: InputEvent -> SceneGraph -> SceneGraph
inputEvent (WindowSizeChangedTo (w,h)) (SceneRoot _ _ sg) = SceneRoot w h sg
inputEvent _ sg = sg

我遇到了这个错误:

No instance for (Monoid SceneGraph) arising from a use of `scene'
Possible fix: add an instance declaration for (Monoid SceneGraph)
In the second argument of `(^.)', namely `scene'
In the expression: g ^. scene
In an equation for `sg': sg = g ^. scene

但我不明白为什么SceneGraph必须是Monoid的实例才能使用这个透镜。

2个回答

17
您可能想要使用(^?),或者可能是(^..)(非运算符名称:previewtoListOf)。
当您拥有一个Lens(或GetterIsoEquality等)时,它总是指向确切的一项。因此,您可以使用普通的(^.)(非运算符名称:view)。当您拥有一个Traversal(或者FoldPrism等)时,它可以引用0个或多个项目。
因此,如果有多个项目,必须有一种方法将它们组合起来,或者给出默认值。这是使用Monoid约束完成的。toListOf为您提供所有值的列表;preview为您提供Nothing或第一个值的Just
由于未给出您使用的任何函数的类型,因此我无法确定您的意图。我猜您的意思可能是scene可能失败,因为您使用了makeLenses与不在每个和式中定义scene的和类型。在这种情况下,您可能需要使用(^?)并处理Nothing情况。但这可能是其他问题。
另请参见我对此问题的回答(以及昨天的此问题!这似乎是一个热门话题)。

啊!这就是为什么makeLenses会创建一个棱镜的原因。 - J. Abrahamson
2
好的,我想我明白了。这是因为我的数据类型看起来像这样:data Game = GameLoad | Game { _scene :: SceneGraph } | GameOver,所以由于游戏是其他两个构造函数之一,scene 可能会失败... - schellsan
啊哈。是的。你需要处理GameLoad/GameOver这两种情况。 - shachaf
在这种情况下,它不是一个棱镜,只是一个(仿射)遍历。虽然它可以是一个棱镜,因为Game只有一个参数(但我认为在这里生成棱镜是有点不直观的行为)。 - shachaf
我今天已经读了这个答案3次,而上面schellsan评论中的直觉最终帮助我理解了它。谢谢! - bojo

2
我假设您正在使用Ed Kmett的镜头库,如果您能够发布您所使用的版本和导入内容,那将会很有帮助。在该镜头库中似乎有两个版本的(^.)被支持,一个是Getter,另一个是Fold,其中Fold版本需要Monoid的实例:(^.) :: Monoid r => s -> Fold s r -> r。
编辑:我猜您不小心导入了Control.Lens.Fold。

3
不,只有一个(^.)。当与折叠/遍历一起使用时,会添加Monoid约束条件。 - shachaf
要求结果是Monoid的优点是什么? - schellsan
2
即使镜头聚焦于 0(mempty)或多个(mconcat)值,它也允许返回单个结果。 - J. Abrahamson

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接