繁体   English   中英

Haskell 记录语法

[英]Haskell record syntax

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

而不是这样的声明:

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

在我看来,沿着这些思路的东西会更有吸引力:

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

我敢肯定一定有我错过的充分理由,但为什么采用类似 C 的记录语法而不是更清晰的基于布局的方法?

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

好吧,如果没有其他人愿意尝试,那么我将再尝试(稍微仔细研究一下)来回答这些问题。

tl;博士

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

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

阅读每个答案的冗长解释,这些解释基于我发现相关且有趣的链接和引用。

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

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

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

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

最有力的理由似乎是没有明显的“正确”设计。

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

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

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

管道中是否有任何解决命名空间问题的方法?

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

稍微切题一点,微软人对 Haskell 的类型类有一些有趣的说法:

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

别忘了哈斯克尔也曾年轻过。 有些决定只是因为它们被做出了。

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

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

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

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

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

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

λ> 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 在该地区自由发表他的脚踏实地的作品。

我只是想添加一个链接来解决名称空间问题。 GHC 的重载记录字段似乎出现在 GHC 7.10 中(并且可能已经在 HEAD 中),使用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)

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

记录句法

在黑暗中试探一下:你的“基于布局”的提议语法看起来很像非记录语法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"}

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

截至 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!

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

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

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

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM