如何用Lens处理重复记录字段?析Haskell代码编译报错问题
问题根源分析
你遇到的这个编译错误,核心原因在于makeFieldsNoPrefix生成共享字段类型类的逻辑:
当你为Bar类型生成字段透镜时,因为Bar2构造函数没有_id字段,所以自动生成的HasId Bar Int实例中,id是一个Traversal(而非普通的Lens)——它表示这个字段可能存在(比如Bar1)也可能不存在(比如Bar2)。
而Traversal的view操作(也就是你用的(^.)运算符)有个要求:目标类型必须实现Monoid实例。这是因为当遍历没有结果时(比如访问Bar2的id),需要用mempty来填充返回值。
虽然你操作的是Foo类型(它的_id字段在所有构造函数中都存在,对应的id应该是合法的Lens),但由于HasId类型类的方法签名是统一的(要兼容Bar的Traversal实例),类型系统会默认把id当成Traversal来处理,进而要求Int实现Monoid——而Int默认没有这个实例,所以编译失败。
最优解决方案:用OverloadedLabels替代makeFieldsNoPrefix
如果你想保留无冗余的字段命名,同时避免类型类共享带来的约束问题,推荐使用OverloadedLabels扩展结合lens的labelOptic功能。这样每个类型的字段透镜都是独立的,不会互相干扰:
{-# LANGUAGE OverloadedLabels #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE DuplicateRecordFields #-} module Foo where import Control.Lens ((^.), makeLenses) import Control.Lens.Labels (labelOptic) import Prelude hiding (id) data Bar = Bar1 { _id :: Int , _name :: String } | Bar2 { _name :: String } makeLenses ''Bar -- 这里改用makeLenses,而非makeFieldsNoPrefix data Foo = Foo { _id :: Int , _name :: String } makeLenses ''Foo a = (undefined :: Foo) ^. #name -- 编译正常 b = (undefined :: Foo) ^. #id -- 现在可以正常编译了! -- 访问Bar的id也符合预期:如果是Bar2,依然需要Monoid Int(因为是Traversal) -- c = (Bar2 "test" :: Bar) ^. #id -- 取消注释会触发Monoid错误,符合逻辑
这个方案的优点:
- 完全不需要修改字段命名(依然用
_id、_name) Foo的#id是Lens,Bar的#id是Traversal,各自的类型约束独立- 语法简洁,只需要在字段名前加
#即可调用对应的透镜
备选方案:手动生成独立透镜
如果你不想启用新的GHC扩展,也可以手动为每个类型生成带前缀的独立透镜,避免共享类型类:
{-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE DuplicateRecordFields #-} module Foo where import Control.Lens ((^.), makeLensesFor) import Prelude hiding (id) data Bar = Bar1 { _id :: Int , _name :: String } | Bar2 { _name :: String } -- 为Bar生成带前缀的透镜 makeLensesFor [("_id", "barId"), ("_name", "barName")] ''Bar data Foo = Foo { _id :: Int , _name :: String } -- 为Foo生成带前缀的透镜 makeLensesFor [("_id", "fooId"), ("_name", "fooName")] ''Foo a = (undefined :: Foo) ^. fooName -- 正常编译 b = (undefined :: Foo) ^. fooId -- 正常编译
这个方案虽然需要给透镜加前缀,但胜在不需要额外扩展,适合保守的场景。
内容的提问来源于stack exchange,提问作者itsu
相关产品推荐
相关产品推荐

