【问题标题】:Avoiding namespace pollution in Haskell在 Haskell 中避免命名空间污染
【发布时间】:2011-05-14 17:09:54
【问题描述】:

我在一个程序中使用了很多不同的记录,其中一些使用相同的字段名称,例如

data Customer = Customer { ..., foo :: Int, ... }
data Product = Product { ..., foo :: Int, ... }

现在,由于访问器函数“foo”被定义了两次,我得到了“多重声明”错误。避免这种情况的一种方法是使用完全限定导入的不同模块,或者只是重命名字段(我不想这样做)。

官方建议在 Haskell 中处理这个问题的方法是什么?

【问题讨论】:

  • 我分担你的痛苦。我来自 OO 世界。
  • 所以看起来我会选择合格的进口产品——至少对于这个项目来说是这样。谢谢大家的答案!这是我想念使用类型类时摆脱 DRY 违规的 Scheme 宏的那些时刻之一......
  • 我发现this project page 是关于 GHC 的 OverloadedRecordFields 扩展,以允许多个记录数据类型共享相同的字段名称。

标签: haskell namespaces types records


【解决方案1】:

这是一个非常棘手的问题。 修复记录系统有几个建议。在相关说明中,请参阅 TDNR 和 related discussion on cafe。

使用当前可用的语言功能,我认为最好的选择是在两个不同的模块中定义这两种类型,并进行合格的导入。除此之外,如果你愿意,你可以实现一些类型类机制。

在 Customer.hs 中

module Customer where
data Customer = Customer { ..., foo :: Int, ... }

在 Product.hs 中

module Product where
data Product = Product { ..., foo :: Int, ... }

在使用它们时,在 Third.hs 中

module Third where

import qualified Customer as C
import qualified Product as P

.. C.foo ..
.. P.foo ..

然而,我想在你遇到recursively dependent modules 的问题之前不会太晚。

【讨论】:

  • 关于递归依赖的要点。但正如链接线程中所述,“我发现缺少此功能迫使我在树状结构中设计模块。这实际上帮助我避免了一些糟糕的设计。有时 [...] 我声明相互依赖通过错误 - 错误消息有助于调试它。”所以现在我将坚持使用完全合格的导入并将我的记录定义拆分为多个文件。也许稍后会采用 typeclass 方法,但现在看起来有点矫枉过正......
【解决方案2】:

(仅供参考,这个问题几乎可以肯定是重复的)

解决方案:

1) 字段前加标签表示类型(非常常见)

data Customer = Customer {..., cFoo :: Int, ...}

2) 使用类型类(不太常见,人们抱怨像 cFoo 这样的前缀不方便,但显然还不错,以至于他们会编写类和实例或使用 TH 来做同样的事情)。

class getFoo a where
    foo :: a -> Int

instance getFoo Customer where
    foo = cFoo

3) 使用更好的字段名称 如果字段实际上不同(并非总是如此,我的计算机和我的员工一样有年龄),那么这是最好的解决方案。

【讨论】:

  • 我使用的更常见的标签语法是 c_foo。
  • typeclass 方法看起来很有前途,但增加了很多代码重复,因为我的数据定义中有许多公共字段(例如 productId、customerId 等),我需要在每个 typeclass 实例中处理这些字段再次。我宁愿使用合格的导入来拥有“真正的”命名空间,而不是重命名。
  • 类型类的做法实在不现实。它需要为每个想要具有公共属性模式(getter、setter、修饰符)的命名字段创建一个单独的类型类。这正是记录语法要避免的。
  • 这个答案已有 4 年历史。当然,现在人们可以使用 Lenses 来满足大多数需求。
【解决方案3】:

有一个语言扩展 DuplicateRecordFields 允许重复字段函数并使其类型可以通过类型注释来推断。

这是一个小例子(haskell-stack 脚本):

#!/usr/bin/env stack
-- stack runghc --resolver lts-8.20 --install-ghc

{-# LANGUAGE DuplicateRecordFields #-}

newtype Foo = Foo { baz :: String }
newtype Bar = Bar { baz :: String }

foo = Foo { baz = "foo text" }
bar = Bar { baz = "bar text" }

main = do
  putStrLn $ "Foo: " ++ baz (foo :: Foo) -- Foo: foo text
  putStrLn $ "Bar: " ++ baz (bar :: Bar) -- Bar: bar text

【讨论】:

    【解决方案4】:

    另见 Has 包:http://chrisdone.com/posts/duck-typing-in-haskell

    如果你现在真的需要可扩展的记录,你总是可以使用 HList。但在你真正熟悉和熟悉中高级 Haskell 之前,我不会推荐这个,即使那样我也会三重检查你是否需要它。

    Haskelldb 有一个更轻量级的版本:http://hackage.haskell.org/packages/archive/haskelldb/2.1.0/doc/html/Database-HaskellDB-HDBRec.html

    还有另一个版本的可扩展记录作为葡萄柚 frp 库的一部分:http://hackage.haskell.org/package/grapefruit-records

    再次,为了您的目的,我会硬着头皮重命名这些字段。但这些参考资料是为了表明,当您真正需要可扩展记录的全部功能时,有办法做到这一点,即使没有一种方法能像精心设计的语言扩展那样令人愉快。

    【讨论】:

    • 哇!刚刚意识到在 Haskell 方面我还有很多东西需要学习。对于我目前的项目,这看起来有点矫枉过正,但我​​会仔细看看你的链接。
    【解决方案5】:

    一种可能使您的代码不那么冗长的解决方案是将<.> 定义为:

    (<.>) :: (Emiter e1, Emiter e2) => e1 -> e2 -> String
    lhs <.> rhs = emit lhs <> emit rhs
    

    然后发射器可能看起来像:

    class Emiter n where
        emit :: n -> String 
    
    instance Emiter String where
        emit = id
    
    instance Emiter A where 
        emit A {
            foo = foo'
            bar = bar'
        } = foo' <.> "--" <.> bar'
    
    instance Emiter B where
        emit B {
            foo = foo'
            bar = bar'
        } =  "[" <.> bar' <.> foo' <.> "]"
    

    【讨论】:

      猜你喜欢
      • 2015-03-05
      • 2020-01-03
      • 1970-01-01
      • 1970-01-01
      • 2023-04-03
      • 1970-01-01
      • 2011-06-04
      • 1970-01-01
      • 2011-10-04
      相关资源
      最近更新 更多