我今天早些时候看过Inverse of the absurd function,虽然我很清楚drusba :: a -> Void
的任何可能实现都不会终止(毕竟,构建Void
是不可能的),我不知道理解为什么absurd :: Void -> a
的情况并非如此。考虑GHC的实施:
newtype Void = Void Void
absurd :: Void -> a
absurd a = a `seq` spin a where
spin (Void b) = spin b
在我看来, spin
无休止地解开了无限系列的Void
newtype包装器,即使你找到Void
传递它也永远不会返回。无法区分的实现类似于:
absurd :: Void -> a
absurd a = a `seq` undefined
鉴于此,为什么我们说absurd
是一个值得存在于Data.Void中的正确函数,但是
drusba :: a -> Void
drusba = undefined
是一个无法定义的函数?它是否像以下那样?
absurd
是一个总函数,为其(空)域中的任何输入提供非底部结果,而drusba
是部分的,给出其中一些(实际上所有)输入的最低结果域。
答案 0 :(得分:19)
由于历史原因,任何Haskell数据类型(包括newtype
)必须至少有一个构造函数。
因此,要在" Haskell98"中定义Void
。一个人需要依赖类型级递归newtype Void = Void Void
。此类型没有(非底部)值。
absurd
函数必须依赖(值级别)递归来处理"怪异的" Void
类型的形式。
在更现代的Haskell中,通过一些GHC扩展,我们可以定义零构造函数数据类型,这将导致更清晰的定义。
{-# LANGUAGE EmptyDataDecls, EmptyCase #-}
data Void
absurd :: Void -> a
absurd x = case x of { } -- empty case
这个案例是详尽无遗的 - 它确实处理了Void
的所有构造函数,它们都是零。因此它是完全的。
在其他一些功能语言中,例如Agda或Coq,上面案例的变体在处理像Void
这样的空类型时是惯用的。
答案 1 :(得分:17)
Data.Void
已从void
包移至基本版base
中的4.8
(GHC 7.10)。如果您查看void
的Cabal文件,您会看到旧版Data.Void
版仅包含base
。 Now, Void
is defined as chi suggests:
data Void
absurd :: Void -> a
absurd a = case a of {}
完全有效。
我认为旧定义背后的想法是这样的:
考虑类型
data BadVoid = BadVoid BadVoid
此类型无法完成工作,因为实际上可以使用该类型定义非底部(coinductive)值:
badVoid = BadVoid badVoid
我们可以通过使用严格注释来解决这个问题,严格注释(至少有点)强制类型是归纳的:
data Void = Void !Void
在可能会或可能不会实际持有,但至少在道德上持有的假设下,我们可以合理地对任何归纳类型执行归纳。所以
spin (Void x) = spin x
如果假设我们有Void
类型的东西,将永远终止。
最后一步是用newtype替换single-strict-constructor数据类型:
newtype Void = Void Void
这总是合法的。然而,由于spin
和data
之间的模式匹配语义不同,newtype
的定义已经改变了含义。为了准确地保留含义,spin
应该写成
spin !x = case x of Void x' -> spin x'
(避免spin !(Void x)
绕过newtype构造函数和爆炸模式之间的交互中现在修复的错误;但是对于GHC 7.10(ha!),这种形式实际上并不会产生所需的错误消息,因为它是“优化的” “进入无限循环”,此时absurd = spin
。
值得庆幸的是,它最终并不重要,因为整个旧定义有点愚蠢。