为什么Haskell的`head`在空列表中崩溃(或者为什么*不返回空列表)? (语言哲学)

时间:2011-06-15 21:09:02

标签: haskell polymorphism parametric-polymorphism

请注意其他潜在的贡献者:请不要犹豫,使用抽象或数学符号来说明您的观点。如果我发现你的答案不清楚,我会要求澄清,但你可以随意以舒适的方式表达自己。

要明确:我正在寻找“安全”headhead的选择也没有特别重要的意义。问题的关键在于headhead'的讨论,它们用于提供背景。

我已经在几个月内一直在攻击Haskell(到了它已经成为我的主要语言),但我确实不了解一些更先进的概念,也没有了解语言的细节。哲学(虽然我更愿意学习)。我的问题不是技术问题(除非它是,我只是没有意识到),因为它是哲学问题。

对于这个例子,我说的是head

我想你会知道,

Prelude> head []    
*** Exception: Prelude.head: empty list

这来自head :: [a] -> a。很公平。显然,一个人不能返回(挥手)没有类型的元素。但与此同时,定义

很简单(如果不是微不足道的话)
head' :: [a] -> Maybe a
head' []     = Nothing
head' (x:xs) = Just x

我在某些陈述的评论部分看到过对here的一些讨论。值得注意的是,一位Alex Stangl说道

  

'有充分的理由不让一切都“安全”,并在违反前提条件时抛出异常。'

我不一定质疑这个断言,但我很好奇这些“好理由”是什么。

此外,保罗约翰逊说,

  

'例如,您可以定义“safeHead :: [a] - >也许是”,但现在不是处理空列表或证明它不会发生,您必须处理“Nothing”或证明它不可能发生。'

我从该评论中读到的语气表明,这是一个显着的难度/复杂性/事物的增加,但我不确定我是否掌握了他在那里推出的内容。

One Steven Pruzina说(2011年,不会少),

  

“有一个更深层次的原因,例如'head'不能防止崩溃。要成为多态但处理空列表,'head'必须始终返回任何特定空列表中不存在的类型变量。如果Haskell可以这样做,那将是Delphic ......“。

允许空列表处理会导致多态性丢失吗?如果是这样,怎么样,为什么?是否有特殊情况会使这一点变得明显? 本节由@Russell O'Connor充分回答。当然,任何进一步的想法都会受到赞赏。

我会根据清晰度和建议来编辑这个。您可以提供的任何想法,论文等都将非常感激。

6 个答案:

答案 0 :(得分:95)

  

允许空的是多态性丢失了   清单处理?如果是这样,怎么样,为什么?   是否会有特殊情况   这显而易见吗?

head的自由定理表明

f . head = head . $map f

将此定理应用于[]意味着

f (head []) = head (map f []) = head []

这个定理必须适用于每个f,因此特别是它必须适用于const Trueconst False。这意味着

True = const True (head []) = head [] = const False (head []) = False

因此,如果head具有适当的多态性且head []是总值,则True将等于False

PS。我还有一些关于你的问题背景的评论,如果你有一个前提条件,你的列表是非空的,那么你应该通过在函数签名中使用非空列表类型而不是使用列表来强制执行它。

答案 1 :(得分:23)

为什么有人使用head :: [a] -> a代替模式匹配?其中一个原因是因为您知道参数不能为空且不想编写代码来处理参数为空的情况。

当然,head'类型[a] -> Maybe a在标准库中定义为Data.Maybe.listToMaybe。但是,如果您将head的使用替换为listToMaybe,则必须编写代码来处理空案例,这会使head的目的无效。

我并不是说使用head是一种很好的风格。它隐藏了它可能导致异常的事实,从这个意义上来说它并不好。但它有时很方便。关键是head用于listToMaybe无法提供的某些用途。

问题中的最后一个引用(关于多态)只是意味着不可能定义一个类型为[a] -> a的函数,它在空列表中返回一个值(正如Russell O'Connor在{{3}中解释的那样) })。

答案 2 :(得分:8)

期望以下内容是很自然的:xs === head xs : tail xs - 列表与其第一个元素相同,其次是其余元素。似乎合乎逻辑,对吧?

现在,让我们在将:所谓的“法律”应用于[]时,忽略实际元素,计算一致数([]的应用):foo : bar应与{相同} {1}},但前者有0个,而后者有(至少)一个。哦,哦,有些东西不在这里!

Haskell的类型系统尽管有其优势,但并不能表达出你应该只在非空列表上调用head这一事实(并且'law'仅对非空列表有效) )。使用head将证明的负担转移给程序员,程序员应该确保它不在空列表中使用。我相信像Agda这样依赖类型的语言可以在这里提供帮助。

最后,一个稍微更具操作性的哲学描述:如何实现head ([] :: [a]) :: a?不可能凭空捏造类型a是不可能的(想想无人居住的类型,如data Falsum),并且相当于证明任何东西(通过Curry-Howard同构)。

答案 3 :(得分:4)

有许多不同的方法可以考虑这个问题。因此,我将支持赞成和反对head'

反对head'

没有必要head':由于列表是具体的数据类型,您可以通过模式匹配来完成head'所做的一切。

此外,对于head',您只需将一个仿函数换成另一个仿函数。在某些时候,你想要深入了解并在底层列表元素上完成一些工作。

head'辩护:

但模式匹配模糊了正在发生的事情。在Haskell中,我们对计算函数感兴趣,通过使用合成和组合器以无点样式编写它们可以更好地完成。

此外,考虑[]Maybe仿函数,head'允许您在它们之间来回移动(特别是Applicative []实例} pure = replicate。)

答案 4 :(得分:2)

如果在您的用例中,空列表完全没有意义,您可以选择使用NonEmpty,而neHead可以安全使用。如果你从那个角度看到它,那不是head函数是不安全的,它是整个列表数据结构(同样,对于那个用例)。

答案 5 :(得分:1)

我认为这是一个简单和美丽的问题。当然,这是在旁观者的眼中。

如果来自Lisp背景,您可能会发现列表是由cons单元格构建的,每个单元格都有一个数据元素和指向下一个单元格的指针。空列表本身不是列表,而是特殊符号。而哈斯克尔则采用了这种推理。

在我看来,它更清晰,更容易推理,更传统,如果空列表和列表是两个不同的东西。

...我可以补充 - 如果你担心头部不安全 - 不要使用它,请使用模式匹配:

sum     [] = 0
sum (x:xs) = x + sum xs