15

很不言自明。我知道makeClassy应该创建类型类,但我认为两者之间没有区别。

PS。解释两者的默认行为的奖励积分。

4

3 回答 3

14

注意:此答案基于镜头 4.4 或更高版本。那个版本的 TH 有一些变化,所以我不知道它有多少适用于旧版本的镜头。

镜头 TH 功能的组织

镜头 TH 功能都基于一个功能,makeLensesWith(也称为makeFieldOptics镜头内)。此函数接受一个LensRules参数,该参数准确描述了生成的内容以及生成方式。

所以要比较makeLensesmakeFields,我们只需要比较LensRules他们使用的 。您可以通过查看源代码找到它们:

制作镜头

lensRules :: LensRules
lensRules = LensRules
  { _simpleLenses    = False
  , _generateSigs    = True
  , _generateClasses = False
  , _allowIsos       = True
  , _classyLenses    = const Nothing
  , _fieldToDef      = \_ n ->
       case nameBase n of
         '_':x:xs -> [TopName (mkName (toLower x:xs))]
         _        -> []
  }

生成字段

defaultFieldRules :: LensRules
defaultFieldRules = LensRules
  { _simpleLenses    = True
  , _generateSigs    = True
  , _generateClasses = True  -- classes will still be skipped if they already exist
  , _allowIsos       = False -- generating Isos would hinder field class reuse
  , _classyLenses    = const Nothing
  , _fieldToDef      = camelCaseNamer
  }

这些是什么意思?

现在我们知道不同之处在于、simpleLensesgenerateClasses选项。但这些选项实际上意味着什么?allowIsosfieldToDef

  • makeFields永远不会产生改变类型的光学元件。这由simpleLenses = True选项控制。该选项在当前版本的镜头中没有黑线鳕。但是,lens HEAD 为其添加了文档:

     -- | Generate "simple" optics even when type-changing optics are possible.
     -- (e.g. 'Lens'' instead of 'Lens')
    

    所以makeFields永远不会产生类型改变的光学元件,但makeLenses如果可能的话。

  • makeFields将为字段生成类。所以对于每个字段foo,我们都有一个类:

    class HasFoo t where
      foo :: Lens' t <Type of foo field>
    

    这由generateClasses选项控制。

  • makeFields永远不会生成Iso's,即使那是可能的(由allowIsos选项控制,似乎不是从 导出的Control.Lens.TH

  • 虽然makeLenses只为以下划线开头的每个字段生成一个顶级镜头(下划线后的第一个字母小写),但makeFields将为HasFoo类生成实例。它还使用了不同的命名方案,在源代码的注释中进行了解释:

    -- | Field rules for fields in the form @ prefixFieldname or _prefixFieldname @
    -- If you want all fields to be lensed, then there is no reason to use an @_@ before the prefix.
    -- If any of the record fields leads with an @_@ then it is assume a field without an @_@ should not have a lens created.
    camelCaseFields :: LensRules
    camelCaseFields = defaultFieldRules
    

    所以makeFields也期望所有字段不只是以下划线为前缀,还包括数据类型名称作为前缀(如 中data Foo = { _fooBar :: Int, _fooBaz :: Bool })。如果要为所有领域生成镜头,可以省略下划线。

    这一切都由_fieldToDef(导出为lensFieldby Control.Lens.TH)控制。

如您所见,该Control.Lens.TH模块非常灵活。使用,如果您需要标准功能未涵盖的模式makeLensesWith,您可以创建自己的模式。LensRules

于 2014-08-30T20:56:03.143 回答
3

免责声明:这是基于对工作代码的试验;它为我提供了足够的信息来继续我的项目,但我仍然更喜欢有更好记录的答案。

data Stuff = Stuff {
    _foo
    _FooBar
    _stuffBaz
}

makeLenses

  • 将创建foo为镜头访问器Stuff
  • 将创建fooBar(将大写名称更改为小写);

makeFields

  • 将创建baz一个类HasBaz;它将创建Stuff该类的一个实例。
于 2014-08-30T19:41:11.753 回答
3

普通的

makeLenses为类型中的每个字段创建一个顶级光学。它查找以下划线 ( ) 开头的字段,并为该字段_创建尽可能通用的光学。

  • 如果您的类型有一个构造函数和一个字段,您将获得一个Iso.
  • 如果你的类型有一个构造函数和多个字段,你会得到很多Lens.
  • 如果你的类型有多个构造函数,你会得到很多Traversal.

优雅

makeClassy创建一个包含您的类型的所有光学元件的单个类。此版本用于轻松将您的类型嵌入到另一个更大的类型中,从而实现一种子类型化。Lens并且Traversal光学将根据上述规则创建(Iso被排除在外,因为它阻碍了子类型化行为。)

除了每个字段的类中的一个方法之外,您还将获得一个额外的方法,该方法可以轻松地为其他类型派生此类的实例。就顶级方法而言,所有其他方法都具有默认实例。

data T = MkT { _field1 :: Int, _field2 :: Char }

class HasT a where
  t :: Lens' a T
  field1 :: Lens' a Int
  field2 :: Lens' a Char

  field1 = t . field1
  field2 = t . field2

instance HasT T where
  t = id
  field1 f (MkT x y) = fmap (\x' -> MkT x' y) (f x)
  field2 f (MkT x y) = fmap (\y' -> MkT x y') (f y)

data U = MkU { _subt :: T, _field3 :: Bool }

instance HasT U where
  t f (MkU x y) = fmap (\x' -> MkU x' y) (f x)
  -- field1 and field2 automatically defined

这具有额外的好处,即可以轻松导出/导入给定类型的所有镜头。import Module (HasT(..))

字段

makeFields为每个字段创建一个类,该类旨在在具有给定名称的字段的所有类型之间重用。这更像是记录无法在类型之间共享的字段名称的解决方案。

于 2014-08-30T22:33:13.703 回答