为什么Haskell没有Ruby那样的符号或者Erlang那样的原子?

16

我使用过符号的两种语言是Ruby和Erlang,我始终觉得它们非常有用。

Haskell确实有代数数据类型,但我仍然认为符号会非常方便。一个立即想到的用途是,由于符号同构于整数,您可以在需要整数或字符串“主键”的地方使用它们。

原子的语法糖可能很小 - :something或<something>是一个原子。所有原子都是Atom类型的实例,Atom派生自Show和Eq。例如,您可以将其用于更具描述性的错误代码。

type ErrorCode = Atom
type Message = String
data Error = Error ErrorCode Message
loginError = Error :redirect "Please login first"
在这种情况下,使用“:redirect”比使用字符串(“redirect”)更有效,比使用整数(404)更容易理解。 此好处可能看起来微不足道,但我认为将原子作为语言特性(或至少作为GHC扩展)值得添加。 那么,为什么没有将符号添加到语言中呢?或者我想错了吗?

1
错误代码等情况不是需要预定义一组值,而不是允许任意可能是无意义的东西吗?想必其他地方会有处理错误的代码,你需要确保只给它它知道如何处理的东西。 - C. A. McCann
不一定。我可能想在想到错误时使用错误代码,而无需先定义整个错误集作为数据类型。处理程序代码可以简单地处理它想要处理的情况,同时将其余部分归为默认处理程序。 - Anupam Jain
这似乎不是Haskell的最佳实践。但即便如此,我认为sclv提到的库已经足够了,所以我仍然不明白为什么这会有很大的区别。 - C. A. McCann
5个回答

20

我同意camccann的答案,认为这可能是因为它需要深入实现,并且对于这种复杂程度来说用处太小。在Erlang(和Prolog和Lisp)中,符号(或原子)通常用作特殊标记,并且大多数情况下与构造函数具有相同的概念。在Lisp中,动态环境包括编译器,因此它部分地也是一个(有用的)编译器概念泄漏到运行时。

问题在于,符号表内插入符号是不纯的(它修改了符号表)。然而,由于我们从不修改现有对象,因此它具有引用透明性,但是如果天真地实现,可能会导致运行时空间泄漏。事实上,如当前在Erlang中实现的那样,您可以通过将过多的符号/原子内插(当前限制为2^20,我想),因为它们永远无法进行垃圾回收,而导致VM崩溃。在并发设置中实现它也很困难,而不使用巨大的锁定符号表。

但是,这两个问题都可以解决(并且已经解决)。例如,请参见Erlang EEP 20。我在simple-atom包中使用了这种技术。它在底层使用unsafePerformIO,但只在(希望)罕见的情况下使用。它仍然需要GC的帮助来执行类似于间接缩短的优化。它还在内部使用了相当多的IORef,这对性能和内存使用不太好。

总之,这是可行的,但正确实现并不容易。编译器编写者总是权衡一个特性的能力与其实现和维护的努力,似乎一流的符号在这方面输了。


提供有关实现原子的成本更多信息,我开始同意在Haskell中原子可能并不是非常有用。 - Anupam Jain

14

我认为最简单的答案是,对于 Lisp 风格符号(这也是 Ruby 和 Erlang 得到灵感的地方),在 Haskell 中大多数情况下是:

  • 已经通过其他方式实现了 - 例如带有一堆零元构造器的数据类型,它们也可以作为“整数方便名称”。

  • 难以适应 - 存在于语言语法层面的东西通常会有更多的类型信息与之关联,但符号必须要么是彼此独立的不同类型(没有某种轻量级的临时和类型几乎没用),要么都是相同类型(在这种情况下它们与仅使用字符串几乎没有区别)。

此外,请记住,Haskell 本身实际上是一种非常小的语言。很少有“内置”的部分,而那些“内置”的部分中大多数只是其他原语的语法糖。如果包括一堆 GHC 扩展,这就不完全正确了,但带有 -XAndTheKitchenSinkToo 的 GHC 不同于 Haskell 本身。

此外,Haskell 对于伪语法和元编程非常友好,因此即使没有内置功能,您也可以做很多事情。特别是如果你涉及到 TH 和可怕的类型元编程以及其他东西。

所以,归根结底,大多数符号的实用性已经可以通过其他功能获得,而那些不可用的东西要添加起来比价值还要困难。


1
我非常确定Erlang的想法来自Prolog,而Prolog可能受到Lisp的影响,但同样有可能受到1960年代其他研究语言的影响。 - Fred Foo
@larsmans:哦,好观点。我不知道为什么我忘记了Prolog对Erlang的影响。那确实更有意义。 - C. A. McCann
3
艾兰语绝对是从普罗格语中得到的,而普罗格语则来源于LISP语言。 - augustss
原子的添加似乎是Haskell中非常易于使用和实现的变化。此外,我可以想象它会导致简洁而美丽的Haskell代码,这对许多Haskeller来说很重要。如果我们必须使用“可怕”的TH /元编程,那么这种美感和简洁性就会丧失。 - Anupam Jain
@Anupam Jain:您心中所想的特殊语法是什么样子的?您期望这个功能如何使用?对我来说,该项改进的好处似乎相当微小。 - C. A. McCann
我已经编辑了问题,提供了语法糖的示例。 - Anupam Jain

9

语言本身并未提供原子(atom)功能,但可以通过库文件实现:

http://hackage.haskell.org/package/simple-atom

虽然Hackage上还有其它几个相关的库文件,但这个看起来是最新且维护得最好的。


谢谢,看起来很有趣!但正如我在上面的评论中所说,如果我们没有内置的原子语法糖,那么它们的美观和简洁就会丧失,这是使用它们的主要动力因素。 - Anupam Jain

3
Haskell使用类型构造器而不是符号,这样函数可以接受的符号集合就是封闭的,并且可以通过类型系统进行推理。你可以向语言中添加符号,但这将使你陷入使用字符串的境地 - 你必须在运行时检查所有可能的符号与少数已知含义的符号进行比较,添加大量的错误处理等。这将是一个巨大的编译时检查的变通方法。
字符串和符号之间的主要区别在于"interning"(内部化) - 符号是原子的并且可以在常数时间内进行比较。两者都是具有本质上无限数量不同值的类型,与Haskell使用有限类型指定参数和结果的方式背道而驰。
注:我对OCaml比Haskell更熟悉,因此“类型构造器”可能不是正确的术语。例如像None或Just 3这样的东西。

2
为了更加准确,"类型构造器"构造类型,而"数据构造器"构造数据,即值。因此,Nothing :: Maybe aJust :: a -> Maybe a是数据构造器,而Maybe :: * -> *是类型构造器。毫无疑问,您会因这个重要的细节而感到更加启迪。;) - C. A. McCann
字符串和原子之间的区别在于,您可以在运行时构造新字符串,但不能以任何方式操作原子。它们应该只有预定义的Show和Eq实例(可能还包括Ord)。这应该允许有效的编译策略。 - Anupam Jain

1
一个立即想到的用途是,由于符号同构于整数,因此您可以在需要使用整数或字符串“主键”的地方使用它们。

请改用枚举

data FileType = GZipped | BZipped | Plain
  deriving Enum

descr ft  =  ["compressed with gzip",
              "compressed with bzip2",
              "uncompressed"] !! fromEnum ft

枚举/代数数据类型的问题在于它们仅在编译时可知值时才有效。原子/内部字符串在像编译器这样需要在运行时确定值的情况下特别方便。 - sclv
@sclv:那我一定误解了OP的意思,因为在编译时无法确定的符号与(固定范围)整数在任何非平凡的方式上都不是同构的。 - Fred Foo
好的,直到你有太多了,那就是一个错误 :-) - sclv
我在考虑编译时已知的原子。但是每次需要使用一个数据结构/枚举都要定义一次有点麻烦。此外,您需要使使用它的每个代码位都可以访问ADT,而符号则会自动理解。 - Anupam Jain
@Anupam 我认为这是一种好的痛苦。使用ADT强制你将“原子”分组成(希望是)有意义的类型。:plain :: Atom 可能意味着很多事情;Plain :: FileType 在其含义上更清晰,并允许编译器帮助你仅在有意义的地方使用它。 - Dan Burton

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