我正在和Haskell一起玩,因为我正在学习这门语言,而我只是找到了一些我不理解的东西而且我无法找到解释。如果我尝试运行此代码:
map (`div` 0) [1,2,3,4]
我得到除以0的异常,这是预期的。 但是,如果我运行此代码:
length (map (`div` 0) [1,2,3,4])
我得到4!
当我在长度函数中进行映射时,我想知道为什么我没有得到除以0的异常!
答案 0 :(得分:15)
可以通过以下方式定义map
和length
函数:
map :: (a -> b) -> [a] -> [b]
map _ [] = []
map f (x:xs) = f x : map f xs
length :: [a] -> Int
length [] = 0
length (_:xs) = 1 + length xs
现在,让我们手动解决为什么你的第二个例子按照它的方式工作。它是这样的:
length (map (`div` 0) (1:2:3:4:[]))
= length (1 `div` 0 : map (`div` 0) (2:3:4:[])) -- second map equation
= 1 + (length (map (`div` 0) (2:3:4:[]))) -- second length equation
= 1 + (length (2 `div` 0 : map (`div` 0) (3:4:[]))) -- second map equation
.
.
.
= 1 + (1 + (1 + (1 + length (map (`div` 0) []))))) -- second length equation
= 1 + (1 + (1 + (1 + length [])))) -- first map equation
= 1 + (1 + (1 + (1 + 0)))) -- first length equation
= 4 -- arithmetic
这里的诀窍是什么?在Haskell中,评估表达式的过程称为强制它。强制表达式执行必要的最少工作,以便找出结果的最外层数据构造函数。作为其中的一部分,子表达式将仅在必要时被强制实现目标。
所以在这个例子中,我们强制的最外层表达式是length
函数的应用程序。 length
的定义有两种情况,一种使用[]
列表构造函数,另一种使用(:)
构造函数,因此要应用length
我们需要弄清楚这两种情况适用于论证。由于参数的最外层位置没有构造函数,因此我们必须强制它查找。这就是上面推导的第一行和第二行之间的步骤;我们通过查看其参数并选择第二个map
等式强制map
子表达式。
但在此之后,我们获得了确定length
的两个方程式中的哪一个所需的所有信息,因此我们按照“最外层的第一”规则并应用适当的length
方程。在这种情况下,这将丢弃包含除以零的子表达式,这意味着永远不会强制子表达式,并且永远不会触发错误。
然而,在您的第一个示例中,当您将表达式键入GHCI时,您隐式要求解释器打印其结果。这要求它强制列表的主干访问其元素,并强制元素本身打印它们。因此,当您强制列表的第一个元素时,会发生除零错误。
编辑:让我指出你可能没有注意到的一个细微差别。当我们在GHCI中尝试你的第一个例子时,这是会话的结果:
*Main> map (`div` 0) [1,2,3,4]
[*** Exception: divide by zero
在第二行的开头看到那个孤独的开口方括号?在发生除零错误之前,这是正在打印的列表的左括号。同样,请注意此示例中发生的情况:
*Main> map (20 `div`) [1,2,0,4]
[20,10,*** Exception: divide by zero
结果列表的前两个元素,甚至是将第二个元素与第三个元素分开的逗号,打印成功,因为Haskell在需要打印之前不会尝试计算第三个列表元素。
答案 1 :(得分:5)
如果在解释器中键入map
表达式,它将对其进行求值,然后打印结果列表。为此,需要对结果列表中的所有元素进行求值,因为它们将成为显示的字符串的一部分。
但是,当解释器评估length
表达式时,它只需要查看结果列表的结构。它不必查看列表中的实际元素。因此,由于Haskell是一种惰性语言,它只评估它所拥有的内容,这意味着不会对元素进行求值,因此不会抛出任何异常。
答案 2 :(得分:5)
这是一些很好的老Haskell懒惰的评价!如果Haskell不需要计算某些东西,它就不会。在这种情况下,您在长度为4的列表上调用map
。就Haskell而言,将map
应用于任何列表将返回相同大小的列表,无论您是什么操作应用。因此,Haskell只是告诉你长度为4,而实际上没有将任何东西除以0。