使用 makeClassy 制作具有相同字段名称的镜头 (TH)

Make Lenses (TH) with the Same Field Name using makeClassy

This question is regarding Edward A. Kmett's lens package (version 4.13)

我有许多不同的 data 类型,所有这些类型都有一个字段表示包含的元素的最大数量(业务规则受 运行 时间变化的影响,而不是集合实现问题.) 我想在所有情况下都将此字段称为 capacity,但我很快 运行 进入名称空间冲突。

我在 lens 文档中看到有一个 makeClassy 模板,但我找不到我理解的文档。此模板功能是否允许我拥有多个具有相同字段名称的镜头?


已编辑: 让我补充一点,我完全有能力 解决 这个问题。我想知道 makeClassy 是否会 解决 问题。

模板是可选的;你总是可以制作自己的 classes 和镜头。

class Capacitor s where
  capacitance :: Lens' s Int

现在任何具有容量的类型都可以成为此 class 的实例。


另一种方法是分解容量:

data Luggage a = Luggage { clothes :: a, capacity :: !Int }

在字段名前加上下划线和数据类型名,然后使用makeFields:

data Structure = Structure { _structureCapacity :: Int }
makeFields ''Structure

data OtherStructure = OtherStructure { _otherStructureCapacity :: String }
makeFields ''Structure

我发现文档也有点不清楚;必须通过实验弄清楚 Control.Lens.TH 所做的各种事情。

你想要的是 makeFields:

{-# LANGUAGE FunctionalDependencies
           , MultiParamTypeClasses
           , TemplateHaskell
  #-}

module Foo
where

import Control.Lens

data Foo
  = Foo { fooCapacity :: Int }
  deriving (Eq, Show)
$(makeFields ''Foo)

data Bar
  = Bar { barCapacity :: Double }
  deriving (Eq, Show)
$(makeFields ''Bar)

然后在 ghci 中:

*Foo
λ let f = Foo 3
|     b = Bar 7
| 
b :: Bar
f :: Foo

*Foo
λ fooCapacity f
3
it :: Int

*Foo
λ barCapacity b
7.0
it :: Double

*Foo
λ f ^. capacity
3
it :: Int

*Foo
λ b ^. capacity
7.0
it :: Double

λ :info HasCapacity 
class HasCapacity s a | s -> a where
  capacity :: Lens' s a
    -- Defined at Foo.hs:14:3
instance HasCapacity Foo Int -- Defined at Foo.hs:14:3
instance HasCapacity Bar Double -- Defined at Foo.hs:19:3

所以它实际做的是声明一个 class HasCapacity s a,其中容量是从 s 到 a 的镜头(一旦 s 已知,a 就固定了)。它通过从字段中剥离数据类型的(小写的)名称来找出名称 "capcity";我发现不必在字段名称或镜头名称上使用下划线是令人愉快的,因为有时记录语法实际上是您想要的。您可以使用 makeFieldsWith 和各种 lensRules 为计算镜头名称提供一些不同的选项。

如果有帮助,请使用 ghci -ddump-splices Foo.hs:

[1 of 1] Compiling Foo              ( Foo.hs, interpreted )
Foo.hs:14:3-18: Splicing declarations
    makeFields ''Foo
  ======>
    class HasCapacity s a | s -> a where
      capacity :: Lens' s a
    instance HasCapacity Foo Int where
      {-# INLINE capacity #-}
      capacity = iso (\ (Foo x_a7fG) -> x_a7fG) Foo
Foo.hs:19:3-18: Splicing declarations
    makeFields ''Bar
  ======>
    instance HasCapacity Bar Double where
      {-# INLINE capacity #-}
      capacity = iso (\ (Bar x_a7ne) -> x_a7ne) Bar
Ok, modules loaded: Foo.

所以第一个地方创建了 class HasCapcity 并为 Foo 添加了一个实例;第二个使用现有的 class 并为 Bar 创建了一个实例。

如果您从另一个模块导入 HasCapcity class,这也有效; makeFields 可以向现有 class 添加更多实例,并将您的类型分散到多个模块中。但是,如果您在 尚未 导入 class 的另一个模块中再次使用它,它将生成一个 new class(同名),你将有两个不兼容的独立超载 capacity 镜头。


makeClassy 有点不同。如果我有:

data Foo
  = Foo { _capacity :: Int }
  deriving (Eq, Show)
$(makeClassy ''Foo)

(注意 makeClassy 更喜欢在字段上使用下划线前缀,而不是数据类型名称)

然后,再次使用 -ddump-splices:

[1 of 1] Compiling Foo              ( Foo.hs, interpreted )
Foo.hs:14:3-18: Splicing declarations
    makeClassy ''Foo
  ======>
    class HasFoo c_a85j where
      foo :: Lens' c_a85j Foo
      capacity :: Lens' c_a85j Int
      {-# INLINE capacity #-}
      capacity = (.) foo capacity
    instance HasFoo Foo where
      {-# INLINE capacity #-}
      foo = id
      capacity = iso (\ (Foo x_a85k) -> x_a85k) Foo
Ok, modules loaded: Foo.

它创建的class是HasFoo,而不是HasCapacity;它是说,从任何你可以获得 Foo 的地方,你也可以获得 Foo 的容量。 class hard-codes 容量是 Int,而不是像 makeFields 那样重载它。所以这仍然有效(因为 HasFoo Foo,您只需使用 id 获得 Foo):

*Foo
λ let f = Foo 3
| 
f :: Foo

*Foo
λ f ^. capacity
3
it :: Int

但是你不能用这个capcity镜头同时获得不相关类型的容量。