如何处理带镜头的重复记录字段?

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,当你在 FoomakeFields 时,它会重新使用相同的类型 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 的有效镜头。这取决于你想如何处理这种情况。