为什么Haskell/GHC不支持记录名称重载

15

我是Haskell的新手。我注意到Haskell不支持记录名称重载:

-- Records.hs

data Employee = Employee
  { firstName :: String
  , lastName :: String
  , ssn :: String
  } deriving (Show, Eq)

data Manager = Manager
  { firstName :: String
  , lastName :: String
  , ssn :: String
  , subordinates :: [Employee]
  } deriving (Show, Eq)

当我编译这段代码时,会出现以下错误:

[1 of 1] Compiling Main             ( Records.hs, Records.o )

Records.hs:10:5:
    Multiple declarations of `firstName'
    Declared at: Records.hs:4:5
                 Records.hs:10:5

Records.hs:11:5:
    Multiple declarations of `lastName'
    Declared at: Records.hs:5:5
                 Records.hs:11:5

Records.hs:12:5:
    Multiple declarations of `ssn'
    Declared at: Records.hs:6:5
                 Records.hs:12:5

鉴于 Haskell 类型系统的“强度”,似乎编译器应该很容易确定要访问哪个字段。

emp = Employee "Joe" "Smith" "111-22-3333"
man = Manager "Mary" "Jones" "333-22-1111" [emp]
firstName man
firstName emp

我是否存在某些看不到的问题?我知道Haskell报告不允许这样做,但为什么不允许?


1
这并不是你问题的答案,但当我遇到像你这样的情况时,我通常会将数据类型拆分为单独的模块。例如,我可能会创建一个Employee模块和一个Manager模块,并分别导入它们作为EM,然后使用E.firstNameM.firstName等。这给了我相当好的语法。(我并不是说这一定是一个好主意,但这是我最终做出的决定,在我的情况下效果很好)。 - gspr
3
没错,但这似乎是一个优雅语言中的“临时修补”。 - Ralph
3个回答

16

历史原因。有许多竞争性设计用于改进Haskell的记录系统——实际上有很多,以至于还没有达成共识。

这里有一些相关的链接:记录可扩展记录共识


10年过去了,现在有共识了吗?OverloadedRecordDot看起来不错。 - Lynn

9

目前的记录系统并不是非常复杂。它主要是一些语法糖,如果没有记录语法,你可以使用样板文件完成同样的事情。

特别地,这段代码:

data Employee = Employee
  { firstName :: String
  , lastName :: String
  , ssn :: String
  } deriving (Show, Eq)

生成(除其他外)一个名为firstName :: Employee -> String的函数。

如果在同一模块中允许使用以下类型:

data Manager = Manager
  { firstName :: String
  , lastName :: String
  , ssn :: String
  , subordinates :: [Employee]
  } deriving (Show, Eq)

那么,firstName函数的类型会是什么?
它必须是两个重载了相同名称的不同函数,而Haskell不允许这样做。除非您想象这将隐式生成一个类型类并为所有具有名为firstName的字段的实例生成(在一般情况下,当字段可能具有不同类型时会变得混乱),否则Haskell当前的记录系统将无法支持在同一模块中具有相同名称的多个字段。目前,Haskell甚至没有尝试解决这些问题。
当然,这可以做得更好。但是有一些棘手的问题需要解决,实际上没有人提出的解决方案能够说服所有人,即现在还没有最有前途的发展方向。

我猜你可以创建一个类型类,然后让类型类方法调用特定记录版本,但这会给一种通常不需要的语言添加很多样板代码。 - Ralph
1
如果允许字段重载,则 firstName 函数的类型应为 forall a. a。使用类型推断或显式类型声明,此类型应进行特化。在 Agda 中,记录构造函数就是这样工作的。 - JJJ

4

避免这种情况的一种方法是将数据类型放在不同的模块中,并使用限定导入。这样,您可以在不同的数据记录上使用相同的字段访问器,并保持代码整洁和更易读。

例如,您可以为员工创建一个模块。

module Model.Employee where

data Employee = Employee
  { firstName :: String
  , lastName :: String
  , ssn :: String
  } deriving (Show, Eq)

比如,为经理人员设计一个模块:

module Model.Manager where

import Model.Employee (Employee)

data Manager = Manager
  { firstName :: String
  , lastName :: String
  , ssn :: String
  , subordinates :: [Employee]
  } deriving (Show, Eq)

然后,无论您想在哪里使用这两种数据类型,您都可以导入它们并通过限定名称访问它们,如下所示:

import           Model.Employee (Employee)
import qualified Model.Employee as Employee
import           Model.Manager (Manager)
import qualified Model.Manager as Manager

emp = Employee "Joe" "Smith" "111-22-3333"
man = Manager "Mary" "Jones" "333-22-1111" [emp]

name1 = Manager.firstName man
name2 = Employee.firstName emp

请记住,您正在使用两种不同的数据类型,因此Manger.firstName是另一个函数,而不是Employee.firstName,即使您知道这两种数据类型都代表一个人,每个人都有一个名字。但是,抽象数据类型的范围取决于您,例如,您可以从这些“属性集合”中创建Person数据类型。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接