如何处理带镜头的重复记录字段?
how to deal with duplicate record field with lenses?
给定这段代码:
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE FunctionalDependencies #-}
module Foo where
import Control.Lens ((^.), makeFieldsNoPrefix)
import Prelude hiding (id)
data Bar
= Bar1 { _id :: Int
, _name :: String
}
| Bar2 { _name :: String }
$(makeFieldsNoPrefix ''Bar)
data Foo = Foo { _id :: Int
, _name :: String
}
$(makeFieldsNoPrefix ''Foo)
a = (undefined :: Foo) ^. name -- compiles fine
b = (undefined :: Foo) ^. id -- doesnt compile
{-
• No instance for (Monoid Int) arising from a use of ‘id’
• In the second argument of ‘(^.)’, namely ‘id’
In the expression: (undefined :: Foo) ^. id
In an equation for ‘b’: b = (undefined :: Foo) ^. id
-}
据我所知,似乎 id
需要一个 Monoid 的实例,因为当实例的类型为 Bar2
时 Bar 可能会失败(它没有 id
字段)。
但是因为我正在处理 Foo(它总是有一个 id
字段)它应该可以工作,不是吗?
我知道我可以通过在字段前加上 class 名称来解决这个问题,例如:
data Foo = Foo { _fooId :: Int, _fooName :: String }
但如果有一个不错的解决方案而不会使我的字段名称混乱,那么我会全力以赴:-)
所以,这里的问题部分是由求和类型中使用的记录引起的。我们可以为 Foo 生成 name
的镜头,因为每个构造函数都有一个 name
字段;但只有一个构造函数具有 id
。这会导致 TemplateHaskell 生成以下 HasId
class,您可以通过 运行 :browse Foo
在 ghci 中自己查看:
class HasId s a | s -> a where
id :: Traversal' s a
如您所见,它将类型设置为 Traversal'
。因为只能有一个 HasId
类型 class,当你在 Foo
上 makeFields
时,它会重新使用相同的类型 class,即使它CAN生成镜头,typeclass方法id
只有typeTraversal
,遍历需要Monoid
才能使用^.
如果您在模块中交换 makeFields
调用的顺序,您会注意到类型 class 现在由 id
生成为 Lens
;但现在第二个 makeLenses
调用无法编译,因为它生成的是 Traversal
,而不是镜头。
总而言之,您期望类型class的id
方法根据其使用方式(通过选择镜头或遍历)更改类型,但那是不是 typeclasses 在 Haskell.
中的工作方式
您在这里有几个选项,但您确实需要决定您想要的语义是什么。最安全的是坚持你在这里得到的东西,(使用遍历)并始终使用 preview
a.k.a。 (^?)
访问 id 字段。或者,您可以生成单独的组合器;一个fooId
和一个barId
,一个是镜头,另一个是遍历。或者,您可以手动实现 HasId
类型 class,并在丢失(或调用错误)时提供 'default' id,但这两者都会导致非法镜头。
你可以做一些非常粗暴的事情,并使用类型族或类似的东西来确定每个类型的 id
字段是透镜还是遍历;但这将是单调的,而且真的很难理解。事倍功半。
问题的症结在于,如果类型的构造函数没有 id,您 不能 有 id
的有效镜头。这取决于你想如何处理这种情况。
给定这段代码:
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE FunctionalDependencies #-}
module Foo where
import Control.Lens ((^.), makeFieldsNoPrefix)
import Prelude hiding (id)
data Bar
= Bar1 { _id :: Int
, _name :: String
}
| Bar2 { _name :: String }
$(makeFieldsNoPrefix ''Bar)
data Foo = Foo { _id :: Int
, _name :: String
}
$(makeFieldsNoPrefix ''Foo)
a = (undefined :: Foo) ^. name -- compiles fine
b = (undefined :: Foo) ^. id -- doesnt compile
{-
• No instance for (Monoid Int) arising from a use of ‘id’
• In the second argument of ‘(^.)’, namely ‘id’
In the expression: (undefined :: Foo) ^. id
In an equation for ‘b’: b = (undefined :: Foo) ^. id
-}
据我所知,似乎 id
需要一个 Monoid 的实例,因为当实例的类型为 Bar2
时 Bar 可能会失败(它没有 id
字段)。
但是因为我正在处理 Foo(它总是有一个 id
字段)它应该可以工作,不是吗?
我知道我可以通过在字段前加上 class 名称来解决这个问题,例如:
data Foo = Foo { _fooId :: Int, _fooName :: String }
但如果有一个不错的解决方案而不会使我的字段名称混乱,那么我会全力以赴:-)
所以,这里的问题部分是由求和类型中使用的记录引起的。我们可以为 Foo 生成 name
的镜头,因为每个构造函数都有一个 name
字段;但只有一个构造函数具有 id
。这会导致 TemplateHaskell 生成以下 HasId
class,您可以通过 运行 :browse Foo
在 ghci 中自己查看:
class HasId s a | s -> a where
id :: Traversal' s a
如您所见,它将类型设置为 Traversal'
。因为只能有一个 HasId
类型 class,当你在 Foo
上 makeFields
时,它会重新使用相同的类型 class,即使它CAN生成镜头,typeclass方法id
只有typeTraversal
,遍历需要Monoid
才能使用^.
如果您在模块中交换 makeFields
调用的顺序,您会注意到类型 class 现在由 id
生成为 Lens
;但现在第二个 makeLenses
调用无法编译,因为它生成的是 Traversal
,而不是镜头。
总而言之,您期望类型class的id
方法根据其使用方式(通过选择镜头或遍历)更改类型,但那是不是 typeclasses 在 Haskell.
您在这里有几个选项,但您确实需要决定您想要的语义是什么。最安全的是坚持你在这里得到的东西,(使用遍历)并始终使用 preview
a.k.a。 (^?)
访问 id 字段。或者,您可以生成单独的组合器;一个fooId
和一个barId
,一个是镜头,另一个是遍历。或者,您可以手动实现 HasId
类型 class,并在丢失(或调用错误)时提供 'default' id,但这两者都会导致非法镜头。
你可以做一些非常粗暴的事情,并使用类型族或类似的东西来确定每个类型的 id
字段是透镜还是遍历;但这将是单调的,而且真的很难理解。事倍功半。
问题的症结在于,如果类型的构造函数没有 id,您 不能 有 id
的有效镜头。这取决于你想如何处理这种情况。