Haskell中的"undefined"和Java中的"null"有什么区别?

52

这两个术语的类型都是所有类型的交集(未被占据的)。它们可以在代码中传递而不会失败,直到尝试评估它们。我唯一能看到的区别是,在Java中,有一个漏洞允许对null进行恰好一个操作的评估,即引用相等比较(==),而在Haskell中,undefined根本无法进行评估,否则将抛出异常。这是唯一的区别吗?

编辑

我的真正问题是,为什么在Java中包括null似乎是一个非常糟糕的决定,Haskell如何避免这种情况?在我看来,真正的问题在于,你可以对null做一些有用的事情,比如可以检查它是否为null。因为您可以这样做,所以已成为标准惯例,在代码中传递空值并表示“没有结果”,而不是“此程序存在逻辑错误”。而在Haskell中,没有办法检查一个术语是否会导致底部,而不对其进行评估并使程序崩溃,因此它永远不能用于表示“无结果”的方式。相反,必须使用像Maybe这样的东西。

如果我的术语“评估”听起来有点随意...我试图在这里进行类比,但很难准确表达。我想这表明这个比喻不太准确。


2
让我们看看:一个是用Haskell编写的,另一个是用Java编写的? - Matt Ball
2
@Matt,那只是语法,我指的是语义。 - Tom Crockett
13
Java中与null更接近的对应词是Haskell语言中的Nothing。实际上,Java中的每个对象引用都类似于Haskell语言中的Maybe值。例如,Integer foo = new Integer(5)就像是let foo = Just 5,而Integer foo = null则类似于let foo = Nothing - Chuck
@Chuck,这是一种看待问题的方式,但我认为它并不像直接说null的行为很像undefined,除非你可以执行一个操作来检查一个术语是否评估为null - Tom Crockett
3
实际上,这两种情况都有发生。因为对象引用始终可以为null,按照这个类比,在Java中的所有东西都以fromJust为前缀,如果在Nothing上使用它会产生一个错误。因此,从理论上讲,它更接近于Nothing,但在实践中,语义看起来更像是undefined - C. A. McCann
@ILMTitan 是的,它是使用漏洞运算符 == 进行评估的...你唯一允许(安全地)评估 null 的方式是检查它是否为 null。 - Tom Crockett
2个回答

76
在Haskell中,“undefined”和Java中的“null”有什么区别?
好的,让我们稍微回顾一下。
在Haskell中,“undefined”是“底部(bottom)”值的一个示例(表示为⊥)。这样的值表示程序中任何未定义、卡住或部分状态。
许多不同形式的底部存在:非终止循环、异常、模式匹配失败——基本上是程序中某种意义上未定义的状态。值undefined :: a是将程序置于未定义状态的典型示例。 undefined本身并不特别,它不是固定的,你可以使用任何产生底部的表达式来实现Haskell的undefined。例如,这是undefined的有效实现:
 > undefined = undefined

或者立即退出(旧的Gofer编译器使用此定义):

 > undefined | False = undefined

bottom的主要属性是,如果一个表达式评估为bottom,那么您的整个程序将评估为bottom:程序处于未定义状态。

为什么需要这样的值?在惰性语言中,您经常可以操作存储bottom值的结构或函数,而程序本身不会成为bottom。

例如,无限循环列表是完全有效的:

 > let xs = [ let f = f in f 
            , let g n = g (n+1) in g 0
            ]
 > :t xs
 xs :: [t]
 > length xs
 2

我对列表的元素无能为力:

 > head xs
 ^CInterrupted.

这种无限的操作是 Haskell 如此有趣和表达性的一部分。懒惰的结果是 Haskell 特别关注底部值。
然而,很明显,底部的概念同样适用于 Java 或任何(非全)语言。在 Java 中,有许多表达式产生“底部”值:
- 将引用与 null 进行比较(请注意,不是 null 本身,因为它是被定义的); - 除零; - 数组越界异常; - 无限循环等。
你不能很容易地替换一个底部为另一个底部,在这方面 Java 编译器并没有对底部值进行推理。但是,这样的值确实存在。
总之:
- 在 Java 中,解引用 null 值是产生底部值的一个具体表达式; - 在 Haskell 中,undefined 值是一个通用的产生底部值的表达式,可以在 Haskell 中任何需要底部值的地方使用。
这就是它们的相似之处。
补充:
关于 null 本身的问题:为什么它被认为是不好的形式?
首先,Java 的 null 本质上等同于在 Haskell 中的每个类型 a 上添加一个隐式 Maybe a 。
解引用 null 等同于只对 Just 情况模式匹配:f (Just a) = ... a ...
因此,当传入的值为 Nothing(在 Haskell 中)或 null(在 Java 中)时,程序将达到未定义状态。这是不好的:你的程序会崩溃。
因此,通过向每种类型添加 null ,你只需更容易地意外创建底部值 - 类型不再帮助你。你的语言不再帮助你防止特定类型的错误,这是不好的。
当然,其他底部值仍然存在:例如 undefined 异常或无限循环。向每个函数添加一个新的可能失效模式 -- 解引用 null -- 只会使编写崩溃程序变得更容易。

19
啊,这是个好观点;在Java中,底部值不是null本身,而是null的解引用(dereferencing) - Tom Crockett
1
我同意在每种类型中添加null是不好的,因为类型系统不能帮助你捕获NullPointerException。但是,允许每个值都明确地成为undefined是否也存在同样的问题呢?在像Haskell这样的语言中,恒等式x * 0 = 0不再成立,因为值x可能未定义,即使它的类型是int - ben
3
在Haskell中,一旦你有了undefined,就无法将其更改为非底部的内容。这就是为什么在你的代码正确工作时应该避免使用undefined的原因。在Java中,null是默认值,将x=null属于完全正确的做法。只有当引用错误时才会出现问题。但你可以重新分配一个非null值到x上。所以,在Haskell中,“UndefinedPointerException”只能发生在其他错误之后,而在Java中,NullPointerExceptions是导致失败的最初源头。 - xixixao
1
每种编程语言都允许在任何地方使用底部(bottom)作为一个值。其他编程语言只是没有给它一个特殊的名称,仅此而已。(例如,如果你编写了一个C程序,你可以声明一个返回“int”的函数,但实际上却会无限循环而从不返回任何东西。这是未定义行为!) - MathematicalOrchid
2
@orm 允许使用 let x = x in x 的原因是因为您可以使用递归定义的值来实现非常有用的功能。一个例子是斐波那契数列:let fibs = 0 : 1 : zipWith (+) fibs (tail fibs) in fibs 可以高效地生成无限的斐波那契数列。另一个例子是帕斯卡三角形:let pasc = [1] : fmap (\x -> zipWith (+) (0 : x) (x ++ [0])) pasc in pasc。我还使用递归定义的数据来高效地生成质数,尽管这涉及到几个相互依赖的函数,所以我不能轻松地将它粘贴在这里。 - semicolon
显示剩余3条评论

7

您的描述并不完全正确。您说 null 不能被评估。但是由于java是一种急切的语言,这意味着无论 f 的定义是什么(因为方法参数总是在方法运行之前被评估),f(null) 都会抛出 NPE。

你可以在haskell中传递 undefined 而不会收到异常的唯一原因是haskell是惰性的,只有在需要时才评估参数。

undefinednull 之间的另一个区别是,undefined 是标准库中定义的一个简单值。如果没有在标准库中定义它,您可以自己定义它(例如,通过编写 myUndefined = error "My Undefined)。

在Java中,null 是一个关键字。如果没有 null 关键字,您将无法定义它(进行类似于 haskell 定义的相当操作,即 Object myNull = throw(new Exception()),不起作用,因为表达式会在那里被评估)。


2
@pelotom:这么说并不完全准确。例如,如果在那个点上不对null求值(而且由此也没有推论出foo可能为空!),你就不能编写if (foo != null) - Chuck
2
@pelotom:是的,&&||是非严格的。但方法应用是严格的。因此,如果将任何表达式作为参数传递给方法,则在方法体之前将评估该表达式。您不能说“该表达式将首先被评估,除非该表达式评估为空”-没有办法知道表达式是否评估为空而不进行评估。 - sepp2k
2
@pelotom:除了JLS第15.7节没有任何这样的漏洞。 - ILMTitan
3
无论你多么希望它成为真实,Java 语言规范都有所规定。如果你无法接受 Java 语言规范作为 Java 语言的规范,那我们就没有共同的前提来进行理性的讨论。 - ILMTitan
4
@pelotom: 是的,这就是区别所在:在 Haskell 中,你需要明确可能返回非值的情况,而在 Java 中则不需要。这是文化和语法方便性的问题,而不是语言本身的问题。如果你想把 Haskell 代码写满 error,理论上没有人能阻止你这么做(除了可能会遭到全世界其他 Haskell 程序员的强烈反对)。 - C. A. McCann
显示剩余5条评论

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