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

oxf4rvwz  于 2022-12-29  发布在  其他
关注(0)|答案(3)|浏览(130)

此问题与Edward A. Kmett的lens package(版本4.13)有关
我有许多不同的data类型,所有这些类型都有一个表示所包含元素的最大数量的字段(业务规则会在运行时发生变化,而不是集合实现问题)。在所有情况下,我都希望将此字段称为capacity,但很快就遇到了命名空间冲突。
我在lens文档中看到有一个makeClassy模板,但是我找不到我能理解的文档。这个模板函数允许我拥有多个具有相同字段名称的镜头吗?

EDITED:让我补充一点,我非常有能力围绕这个问题进行编码。我想知道makeClassy是否会解决这个问题。

eimct9ow

eimct9ow1#

我发现文档也有点不清楚;我不得不通过实验来弄清楚控制器、透镜和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

所以它实际上做的是声明一个类HasCapacity s a,这里的capacity是从saLens'(一旦s已知,a就固定了),它通过从字段中剥离数据类型的名称(小写)得出名称capacity;我发现不用在字段名或透镜名上使用下划线是很愉快的,因为有时候记录语法实际上就是你想要的,你可以使用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.

因此,第一个拼接生成了类HasCapacity,并为Foo添加了一个示例;第二个使用现有的类并为Bar创建一个示例。
如果您从另一个模块导入HasCapacity类,也可以这样做; makeFields可以向现有的类添加更多示例,并将类型分散到多个模块中,但是如果您在另一个模块中再次使用它,而您 * 还没有 * 导入该类,它将生成一个 * 新 * 类(具有相同的名称),并且您将拥有两个独立的重载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.

它创建的类是HasFoo,而不是HasCapacity;它说的是,任何东西,只要你能得到一个Foo,你就能得到Foo的容量,还有类,硬编码capacity是一个Int,而不是像makeFields那样重载它,所以这仍然有效(因为HasFoo Foo,在这里您只需使用id即可获得Foo):

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

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

但是你不能用这个capacity透镜也得到一个不相关的类型的能力。

ss2ws0br

ss2ws0br2#

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

class Capacitor s where
  capacitance :: Lens' s Int

现在,任何具有容量的类型都可以成为该类的示例。
另一种方法是分解容量:

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

4xrmg8kj3#

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

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

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

相关问题