为什么Haskell在部分函数中使用bottom而不是null?

6
我正在阅读有关Haskell指称语义的内容(http://en.wikibooks.org/wiki/Haskell/Denotational_semantics), 但我看不出为什么在类型中,底部“值”与“正常”值相比放置在另一个级别,比如为什么不能应用模式匹配。
我认为对底部进行模式匹配会引起麻烦,因为底部还表示非终止计算,但为什么非终止计算和错误应该被视为同样的情况呢?(我假设使用不支持的参数调用部分函数可以被认为是一种错误。)
如果所有的Haskell类型都包含一个可用于模式匹配的类Java-null的值,而不是底部,将会失去哪些有用的属性?
换句话说,让所有的Haskell函数通过提升所有类型与null值一起实现总函数,这是否明智?
(非终止计算是否需要特殊的类型?)

1
http://www.infoq.com/presentations/Null-References-The-Billion-Dollar-Mistake-Tony-Hoare - sclv
@Aivar 最近的面向对象语言,如Kotlin和Ceylon开始区分String和String?,其中后者可以是null,但前者不行,这应该让你重新思考你的问题。我敢打赌,如果Java等语言今天才被发明,null将被禁止或以类似于Haskell的Maybe的方式受到限制。 - Ingo
@Ingo:实际上,在程序员应该始终检查某些东西是“正常”值还是“错误值”的情况下,我完全支持类型系统的支持-我满意在那里使用Maybe/Either。我想更多地考虑异常结果,并且能够匹配它们的可能性。现在我看到我不只是想要null,而是一组异常值。但经过再三考虑,我必须承认,我不确定是否值得匹配任何这些真正的异常结果(例如,OutOfMemory可能并不是)。 - Aivar
2个回答

17

如果不限制语言的图灵完备性,就无法摆脱非终止问题。由于停机问题,我们一般不能检测到非终止并将其替换为一个值。

因此,每种图灵完备语言都有底部(bottom)。

Haskell和Java之间唯一的区别在于Java具有底部 null。Haskell没有后者,这很方便,因为我们就不必检查空值!

换句话说,既然底部在图灵完备世界中是不可避免的,那么除了导致错误之外,也没必要使每个变量都可以为空吧?

还要注意的是,虽然Prelude中的某些函数出于历史原因是偏函数,但现代的Haskell风格倾向于几乎在任何地方编写总函数,并在诸如head等可能是偏函数的函数中使用显式的Maybe返回类型。


2
我认为这不公平。在严格语言中,非终止是一种效果,而在非严格语言中,它是一个值。例如在ML中,你不能有一个“未被占用”的类型的值,尽管你可以有一个函数a -> Void,因为函数而不是类型被提升。 - Philip JF
4
如果你采用指称语义的方法,非终止状态始终是你的语义领域中的一个值。如果你像Bob Harper一样不相信指称语义,你可以声称非终止状态是一种影响,它在ML中不存在。我认为Bob Harper懂得挺多,但我也认为在这个问题上Scott和Strachey比他更了解。 - sclv
1
@sclv,在严格的编程语言中,值的语义域与计算的语义域是不同的。底部存在于后者(没有人会否认这一点),但不存在于前者。类型对应于值的域,因此并不包含底部。 - Andreas Rossberg
1
你有这个主张的来源吗?据我所知,你不能为严格语言拥有一个不区分值和表达式等内容的语义学。这就是为什么我见过的任何类似 ML 的指示符都有类似 [a -> b] = [a] -> ([b] + 1) 这样的东西。 - Philip JF
2
我仍然不认为我理解你的意思。虽然我不是指称语义方面的专家,但我认为CBV语言的标准模型使用pCpo并且没有给出底部。因此,data Nat = Z | S Nat的指称只是$\mathbb{Z}$(离散CPO)。也许我漏掉了什么,但在ML中,您不能有一个表示发散的名称,尽管您可以有发散的表达式。 - Philip JF
显示剩余6条评论

14

尽管我在评论中有些挑剔,但我认为sclv回答了你问题的第一部分。至于

如果所有的Haskell类型都包含一个可匹配模式的类Java-null值而不是底部(bottom),会失去哪些有用的属性呢?

换句话说: 是否明智地通过提升带有空值的所有类型使得所有Haskell函数都变成总函数?

在这里,你似乎在非终止和异常之间划分了一个区别。虽然由于停机问题,无法对非终止进行模式匹配,但为什么不能对异常进行模式匹配呢?

对此,我也提出了一个问题:那些永远不会引发异常的函数怎么办?毕竟,Haskell有总函数。如果已知函数不会抛出异常,我不应该需要模式匹配来确保其非异常。作为一种束缚和规范语言,Haskell自然会想通过类型来传达这种差异。也许可以通过编写:

Integer

对于已知不是异常情况的整数类型

?Integer

对于可能会出现异常的整数类型,答案是我们已经做到了:Haskell在预定义中有一个类型。

data Maybe a = Just a | Nothing

可以理解为“要么是a,要么什么都没有”。“Maybe”类型可以进行模式匹配,因此这个提案对我们没有任何帮助。 (我们还有像“Either”这样的类型,用于更丰富的“可能出错的计算”,以及精美的单子语法/组合器,使这些易于处理)。

那么,为什么还需要异常?在Haskell中,我们无法“捕获”异常,除非在IO单子中。如果我们可以使用MaybeEither完美地模拟异常,为什么要在语言中引入异常呢?

这个问题有几个答案,但核心是Haskell的异常不精确。异常可能会发生,因为您的程序耗尽了内存,或者正在执行的线程被另一个线程杀死,或者各种其他不可预测的原因。此外,通常我们关心哪个异常会出现。那么以下表达式应该得到什么结果?

(error "error 1") + (error "error 2") :: Integer

这个表达式显然应该抛出一个异常,但是是哪种异常呢?(+)在整数上的特化版本会严格检查其两个参数,所以这并没有什么帮助。我们可以决定它是第一个值,但是通常情况下我们会得到

x + y =/= y + x

这将限制我们进行等式推理的选项。Haskell提供了一种具有不精确行为的异常概念,这很重要,因为语言的纯部分具有完全精确的行为,这可能会限制其使用。


谢谢!我同意第一部分,即你应该能够在静态上区分全函数和部分函数,而Maybe / Either是一个很好的方式。 - Aivar
现在,对于不精确的异常和捕获的麻烦——如果Haskell使用一些合适的值(例如Exception String)而不是bottom的话,这个问题不就解决了吗?就像bottom一样,这些值将存在于所有类型中,并通过计算进行传播,但它们将可以进行模式匹配(即可捕获)。通常情况下,人们不会为它们编写case语句,因为它们真的很特殊,但如果有需要的话,也是可以的。 - Aivar
顺便说一下,在上述维基页面(http://en.wikibooks.org/wiki/Haskell/Denotational_semantics#Monotonicity)中有一节说模式匹配底部会很糟糕,因为这会破坏单调性,而单调性是好的,但我不明白为什么它是好的。(如果我们将这些异常值与正确的值放在同一级别,甚至可以保持单调性)。我应该写另一个问题吗? - Aivar
如果“error”被命名为类似于“impossible”这样更清晰的名称,那将是很好的。它不应该用于在正确代码中可能发生的代码路径,并且设计用于当您想要崩溃线程/程序时使用。捕获它的唯一原因是在长时间运行的服务器中写入日志文件。 - singpolyma

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