58

由于其丑陋的语法和命名空间污染,Haskell 的记录语法被许多人认为是对其他优雅语言的一个缺点。另一方面,它通常比基于位置的替代方案更有用。

而不是这样的声明:

data Foo = Foo { 
  fooID :: Int, 
  fooName :: String 
} deriving (Show)

在我看来,这些方面的东西会更有吸引力:

data Foo = Foo id   :: Int
               name :: String
               deriving (Show)

我确信我错过了一定有一个很好的理由,但是为什么类 C 的记录语法被采用了一种更简洁的基于布局的方法呢?

其次,管道中是否有任何东西可以解决命名空间问题,所以我们可以编写id foo而不是fooID foo在未来的 Haskell 版本中?(除了当前可用的基于类型类的冗长解决方法。)

4

4 回答 4

54

好吧,如果没有其他人会尝试,那么我将采取另一个(稍微更仔细地研究)回答这些问题。

tl;博士

问题1:这就是骰子滚动的方式。这是一个偶然的选择,它卡住了。

问题2:是的(排序)。几个不同的政党肯定一直在考虑这个问题。

根据我发现相关且有趣的链接和引语,继续阅读每个答案的冗长解释。

为什么采用类似 C 的记录语法而不是更简洁的基于布局的方法?

微软研究人员写了一篇Haskell 历史论文。第 5.6 节讨论记录。我将引用第一点,这是有见地的:

早期版本的 Haskell 最明显的遗漏之一是缺少记录,提供命名字段。鉴于记录在实践中非常有用,为什么要省略它们?

Microsofties 然后回答他们自己的问题

最强有力的原因似乎是没有明显的“正确”设计。

您可以自己阅读论文以了解详细信息,但他们说 Haskell 最终采用记录语法是因为“数据结构中命名字段的压力”。

到 1993 年 Haskell 1.3 设计开始时,用户对数据结构中命名字段的压力很大,因此委员会最终采用了极简设计……

你问为什么是为什么?好吧,据我了解,如果早期的 Haskeller 有他们的方法,我们可能一开始就没有记录语法。这个想法显然是被那些已经习惯了类 C 语法的人推到 Haskell 上的,他们更感兴趣的是把类 C 的东西放到 Haskell 中,而不是用“Haskell 方式”做事。(是的,我意识到这是一个非常主观的解释。我可能大错特错,但在没有更好的答案的情况下,这是我能得出的最好结论。)

管道中有什么东西可以解决命名空间问题吗?

首先,不是每个人都觉得这是个问题。几周前,一位Racket爱好者向我(和其他人)解释说,具有相同名称的不同函数是一个坏主意,因为这会使“名为 ___ 的函数做什么?”的分析变得复杂。事实上,它不是一种功能,而是多种功能。这个想法对于 Haskell 来说可能是额外的麻烦,因为它使类型推断复杂化。

稍微切线,Microsofties 对 Haskell 的类型类有一些有趣的说法:

Wadler 和 Blott 恰巧在语言设计仍在不断变化的那一刻产生了这个关键想法,这是一个令人愉快的时间巧合。

不要忘记 Haskell 曾经年轻过。有些决定是因为他们做出的而做出的。

无论如何,有一些有趣的方法可以解决这个“问题”:

Type Directed Name Resolution,对 Haskell 的提议修改(在上面的评论中提到)。只需阅读该页面,就会发现它涉及语言的许多领域。总而言之,这不是一个坏主意。已经考虑了很多,所以它不会与东西发生冲突。然而,将其融入现在(更)成熟的 Haskell 语言中仍然需要更多的关注。

Microsoft 的另一篇论文OO Haskell专门提出了对 Haskell 语言的扩展,以支持“临时重载”。它相当复杂,因此您只需自己查看第 4 节即可。它的要点是自动(?)推断“有”类型,并添加一个额外的步骤来进行类型检查,他们称之为“改进”,在下面的选择性引号中模糊地概述:

给定类约束Has_m (Int -> C -> r),只有一个 m 实例与该约束匹配......因为只有一个选择,我们现在应该做出选择,而这又固定rInt。因此,我们得到了预期的类型ff :: C -> Int -> IO Int...[this] 只是一种设计选择,并且基于Has_m是封闭的想法

为不连贯的引用道歉;如果这对您有帮助,那就太好了,否则就去阅读论文。这是一个复杂(但令人信服)的想法。

Chris Done 使用 Template Haskell 在 Haskell 中提供鸭子类型,其方式类似于 OO Haskell 论文(使用“Has”类型)。他网站上的一些交互式会话示例:

λ> flap ^. donald
*Flap flap flap*
λ> flap ^. chris
I'm flapping my arms!

fly :: (Has Flap duck) => duck -> IO ()
fly duck = do go; go; go where go = flap ^. duck

λ> fly donald
*Flap flap flap*
*Flap flap flap*
*Flap flap flap*

这需要一点样板/不寻常的语法,我个人更喜欢坚持使用类型类。但对 Chris Done 表示赞赏,因为他在该地区自由地发表了他脚踏实地的作品。

于 2011-04-02T02:54:00.443 回答
13

我只是想添加一个解决命名空间问题的链接。使用扩展OverloadedRecordFields. _

这将允许语法如

data Person = Person { id :: Int, name :: String }
data Company { name :: String, employees :: [Person] }

companyNames :: Company -> [String]
companyNames c = name c : map name (employees c)
于 2014-02-25T15:53:26.910 回答
6

[编辑] 这个答案只是我对此事的一些随机想法。我推荐我的另一个答案而不是这个答案,因为对于那个答案,我花了更多时间来查找和参考其他人的工作。

记录语法

在黑暗中试探一下:您提出的“基于布局”的语法看起来很像非记录语法data声明;这可能会导致解析混淆(?)

--record
data Foo = Foo {i :: Int, s :: String} deriving (Show)
--non-record
data Foo = Foo Int String deriving (Show)
--new-record
data Foo = Foo i :: Int, s :: String deriving (Show)

--record
data LotsaInts = LI {a,b,c,i,j,k :: Int}
--new-record
data LostaInts = LI a,b,c,i,j,k :: Int

在后一种情况下,究竟:: Int适用于什么?整个数据声明?

使用记录语法的声明(当前)类似于构造和更新语法。对于这些情况,基于布局的语法不会更清晰;你如何解析这些额外的=迹象?

let f1 = Foo {s = "foo1", i = 1}
let f2 = f1 {s = "foo2"}

let f1 = Foo s = "foo1", i = "foo2"
let f2 = f1 s = "foo2"

你怎么知道f1 s是记录更新,而不是功能应用程序?

命名空间

如果您想将类定义id的使用与 Prelude 的混合使用id怎么办?你如何指定你正在使用哪一个?您能想出比合格导入和/或hiding关键字更好的方法吗?

import Prelude hiding (id)

data Foo = Foo {a,b,c,i,j,k :: Int, s :: String}
               deriving (Show)

id = i

ghci> :l data.hs
ghci> let foo = Foo 1 2 3 4 5 6 "foo"
ghci> id foo
4
ghci> Prelude.id f1
Foo {a = 1, b = 2, c = 3, i = 4, j = 5, k = 6, s = "foo"}

这些不是很好的答案,但它们是我得到的最好的答案。我个人认为记录语法没有那么难看。我确实觉得命名空间/模块的东西还有改进的余地,但我不知道如何让它变得更好。

于 2011-03-20T17:41:16.817 回答
0

截至 2021 年 6 月,它已通过三个选择加入的语言扩展和计数实现了一半:

https://gitlab.haskell.org/ghc/ghc/-/wikis/records/overloaded-record-fields

即使启用了所有三个扩展,基本的东西,比如

len2 :: Point -> Double
len2 p = (x p)^2 + (y p)^2  -- fails!

比如说,如果还有一个Quaternion带有xandy字段的类型,它仍然无法工作。你必须这样做:

len2 :: Point -> Double
len2 p = (x (p :: Point))^2 + (y (p :: Point))^2

或这个:

len2 :: Point -> Double
len2 (MkPoint {x = px, y = py}) = px^2 + py^2

即使第一个示例确实有效,它仍然是可选的,因此很可能需要再过 20 年,该扩展才能被任何实际应用程序必须依赖的库广泛采用。

具有讽刺意味的是,像这样的交易破坏者在像 C 这样的语言中不是问题。

不过,有趣的一点是:Idris 2 实际上已经解决了这个问题。不过,它也还没有真正准备好。

于 2021-06-15T04:39:03.007 回答