【问题标题】:how to deal with duplicate record field with lenses?镜头重复记录场如何处理?
【发布时间】:2019-11-29 10:27:34
【问题描述】:

鉴于这段代码:

{-# 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(没有 id 字段)时,Bar 可能会失败。

但由于我正在处理 Foo(它总是有一个 id 字段)它应该可以工作,不是吗?

我知道我可以通过在字段前面加上类名来解决这个问题,例如:

data Foo = Foo { _fooId :: Int, _fooName :: String }

但如果有一个不错的解决方案而不会使我的字段名称混乱,那么我全力以赴:-)

【问题讨论】:

  • 呃,我认为这里至少有三件事通常是个坏主意:1. 为 sum 类型创建记录字段。 (镜头确实在一定程度上解决了随之而来的问题,但正如您所遇到的,这也带来了自己的问题。) 2. 重复记录字段。 (始终是一个半生不熟的概念;只有OverloadedLabels 理解了这一点。)3. 不要为不同的东西重复使用标准名称,尤其是对于像@ 这样基本的东西987654330@.
  • 是的,我知道 1 和 2,但我认为镜头会解决他们的问题 :-(
  • 当与 lens 一起使用时,确定 1 是个坏主意吗?在我看来,hackage.haskell.org/package/lens-4.18.1/docs/Control-Lens.html 上的第一个例子是使用 sum 类型的记录,如果这是一种不好的做法
  • 好吧,认为这是个坏主意。意见可能会有所不同。
  • 您可以通过generic-lens 解决很多此类问题。作为奖励,没有 TH 拼接!

标签: haskell haskell-lens monoids


【解决方案1】:

因此,这里的问题部分是由 sum 类型中使用的记录引起的。我们可以为 Foo 生成 name 的镜头,因为每个构造函数都有一个 name 字段;但只有一个构造函数有id。这会导致 TemplateHaskell 生成以下 HasId 类,您可以通过在 ghci 中运行 :browse Foo 来亲自查看:

class HasId s a | s -> a where
  id :: Traversal' s a

如您所见,它将类型设置为Traversal'。由于只能有一个HasId 类型类,当您在Foo 上使用makeFields 时,它将重新使用相同的类型类,即使它可以生成镜头,类型类方法id 也只有类型@987654333 @,并且您需要 Monoid 才能在遍历时使用 ^.

如果您在模块中交换makeFields 调用的顺序,您会注意到现在生成的类型类是id 作为Lens;但现在第二个makeLenses 调用无法编译,因为它生成的是Traversal,而不是镜头。

总结一下这个问题,您期望类型类的 id 方法会根据它的使用方式(通过选择镜头或遍历)更改类型,但这不是类型类在 Haskell 中的工作方式。

这里有几个选项,但你真的需要决定你想要的语义是什么。最安全的做法是坚持使用您在此处获得的内容(使用遍历)并始终使用 preview a.k.a. (^?) 访问 id 字段。或者,您可以生成单独的组合器;一个fooId 和一个barId,一个是镜头,另一个是遍历。或者,您可以手动实现 HasId 类型类,并在它丢失(或调用错误)时提供“默认”ID,但这两者都会导致非法镜头。

您可以做一些非常粗暴的事情,并确定每个类型的id 字段是使用类型族还是类似的镜头或遍历;但这将是单调的,很难理解。这比它的价值更多。

问题的关键是,如果有一个没有 id 的类型的构造函数,你不能拥有id 的有效镜头。由您决定如何处理这种情况。

【讨论】:

  • 这么详细的回复,太好了。太感谢了 !!我选择使用镜头库,因为我认为它解决了 haskell 记录问题,但我想最简单的解决方案仍然是在我的记录前加上类名:-(
  • 没问题 :) 如果您有兴趣了解有关镜片的更多信息,我将在本月晚些时候发布一本全面的镜片书! leanpub.com/optics-by-example
猜你喜欢
  • 1970-01-01
  • 2017-10-22
  • 2015-05-30
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多