Data.Void
中的absurd
函数具有以下签名,其中Void
是该软件包导出的逻辑无人居住类型:
-- | Since 'Void' values logically don't exist, this witnesses the logical
-- reasoning tool of \"ex falso quodlibet\".
absurd :: Void -> a
我确实知道足够的逻辑来获得文档的评论,即通过命题与类型的对应关系,它与有效的公式⊥ → a
相对应。
我感到困惑和好奇的是:这个函数在什么样的实际编程问题中有用?我想也许在某些情况下它可能是一种类型安全的方式来彻底处理“不可能发生”的情况,但我对Curry-Howard的实际用法不太了解,以确定这个想法是否在正确的轨道。
编辑:最好在Haskell中举例,但如果有人想使用依赖类型的语言,我就不会抱怨......
答案 0 :(得分:58)
考虑这个由其自由变量参数化的lambda项的表示。 (参见Bellegarde和Hook 1994,Bird and Paterson 1999,Altenkirch和Reus 1999的论文。)
data Tm a = Var a
| Tm a :$ Tm a
| Lam (Tm (Maybe a))
你当然可以将其设为Functor
,捕捉重命名的概念,并Monad
捕捉替代的概念。
instance Functor Tm where
fmap rho (Var a) = Var (rho a)
fmap rho (f :$ s) = fmap rho f :$ fmap rho s
fmap rho (Lam t) = Lam (fmap (fmap rho) t)
instance Monad Tm where
return = Var
Var a >>= sig = sig a
(f :$ s) >>= sig = (f >>= sig) :$ (s >>= sig)
Lam t >>= sig = Lam (t >>= maybe (Var Nothing) (fmap Just . sig))
现在考虑已关闭的条款:这些是Tm Void
的居民。您应该能够将封闭的术语嵌入到具有任意自由变量的术语中。怎么样?
fmap absurd :: Tm Void -> Tm a
当然,问题在于,这个函数将遍历完全没有做任何事情的术语。但它比unsafeCoerce
更诚实。这就是为vacuous
添加Data.Void
...
或者写评估员。以下是b
中包含自由变量的值。
data Val b
= b :$$ [Val b] -- a stuck application
| forall a. LV (a -> Val b) (Tm (Maybe a)) -- we have an incomplete environment
我刚刚将lambdas表示为闭包。评估器通过将a
中的自由变量映射到b
以上的值的环境进行参数化。
eval :: (a -> Val b) -> Tm a -> Val b
eval g (Var a) = g a
eval g (f :$ s) = eval g f $$ eval g s where
(b :$$ vs) $$ v = b :$$ (vs ++ [v]) -- stuck application gets longer
LV g t $$ v = eval (maybe v g) t -- an applied lambda gets unstuck
eval g (Lam t) = LV g t
你猜对了。评估任何目标的封闭期限
eval absurd :: Tm Void -> Val b
更一般地说,Void
很少单独使用,但是当你想以一种表明某种不可能性的方式实例化一个类型参数时很方便(例如,在这里,在一个封闭的时候使用一个自由变量)术语)。通常,这些参数化类型具有更高阶函数,将参数的操作提升到整个类型上的操作(例如,这里,fmap
,>>=
,eval
)。因此,您将absurd
作为Void
上的通用操作传递。
再举一个例子,假设使用Either e v
来捕获计算,希望给你一个v
,但可能引发类型e
的异常。您可以使用此方法统一记录不良行为的风险。对于此设置中表现良好的计算,请将e
设为Void
,然后使用
either absurd id :: Either Void v -> v
安全运行或
either absurd Right :: Either Void v -> Either e v
将安全组件嵌入不安全的世界。
哦,最后一次欢呼,处理“不可能发生”。它出现在通用拉链结构中,光标不能到达任何地方。
class Differentiable f where
type D f :: * -> * -- an f with a hole
plug :: (D f x, x) -> f x -- plugging a child in the hole
newtype K a x = K a -- no children, just a label
newtype I x = I x -- one child
data (f :+: g) x = L (f x) -- choice
| R (g x)
data (f :*: g) x = f x :&: g x -- pairing
instance Differentiable (K a) where
type D (K a) = K Void -- no children, so no way to make a hole
plug (K v, x) = absurd v -- can't reinvent the label, so deny the hole!
我决定不删除其余内容,即使它并不完全相关。
instance Differentiable I where
type D I = K ()
plug (K (), x) = I x
instance (Differentiable f, Differentiable g) => Differentiable (f :+: g) where
type D (f :+: g) = D f :+: D g
plug (L df, x) = L (plug (df, x))
plug (R dg, x) = R (plug (dg, x))
instance (Differentiable f, Differentiable g) => Differentiable (f :*: g) where
type D (f :*: g) = (D f :*: g) :+: (f :*: D g)
plug (L (df :&: g), x) = plug (df, x) :&: g
plug (R (f :&: dg), x) = f :&: plug (dg, x)
实际上,也许它是相关的。如果您有冒险精神,那么unfinished article会展示如何使用Void
来压缩带有自由变量的术语表示
data Term f x = Var x | Con (f (Term f x)) -- the Free monad, yet again
在Differentiable
和Traversable
仿函数f
中自由生成的任何语法中。我们使用Term f Void
来表示没有自由变量的区域,使用[D f (Term f Void)]
来表示 tube 隧道穿过没有自由变量的区域到一个孤立的自由变量,或者到一个结点两个或多个自由变量的路径。必须在某个时候完成那篇文章。
对于没有价值的类型(或者至少,没有值得在礼貌的公司中提及),Void
非常有用。 absurd
就是你如何使用它。
答案 1 :(得分:54)
生活有点困难,因为Haskell不严格。一般用例是处理不可能的路径。例如
simple :: Either Void a -> a
simple (Left x) = absurd x
simple (Right y) = y
事实证明这有点用处。考虑Pipes
data Pipe a b r
= Pure r
| Await (a -> Pipe a b r)
| Yield !b (Pipe a b r)
这是来自Gabriel Gonzales'Pipes
库的标准管道类型的严格和简化版本。现在,我们可以将永不产生的管道(即消费者)编码为
type Consumer a r = Pipe a Void r
这真的永远不会产生。这意味着Consumer
的正确折叠规则是
foldConsumer :: (r -> s) -> ((a -> s) -> s) -> Consumer a r -> s
foldConsumer onPure onAwait p
= case p of
Pure x -> onPure x
Await f -> onAwait $ \x -> foldConsumer onPure onAwait (f x)
Yield x _ -> absurd x
或者,您可以在与消费者打交道时忽略产量案例。这是此设计模式的一般版本:使用多态数据类型和Void
在需要时摆脱可能性。
Void
最经典的用法可能就是CPS。
type Continuation a = a -> Void
也就是说,Continuation
是一个永不返回的函数。 Continuation
是“not”的类型版本。由此我们得到一个CPS(对应于经典逻辑)的单子
newtype CPS a = Continuation (Continuation a)
因为Haskell是纯粹的,所以我们无法从这种类型中获得任何东西。
答案 2 :(得分:35)
我在想,或许它在某些情况下有用,作为一种彻底处理“不可能发生”案件的类型安全方式
这是完全正确的。
您可以说absurd
并不比const (error "Impossible")
更有用。但是,它是类型限制的,因此它的唯一输入可以是类型Void
的类型,这是一种故意无人居住的数据类型。这意味着您无法传递给absurd
的实际值。如果您最终在代码分支中,类型检查器认为您可以访问Void
类型的某些内容,那么,您处于荒谬状态。所以你只需使用absurd
来基本上标记永远不会达到这个代码分支。
“Ex falso quodlibet”字面意思是“来自[a]虚假[命题],任何事情都遵循”。因此,当您发现自己持有类型为Void
的数据时,您就会知道手中有错误的证据。因此,你可以填写你想要的任何洞(通过absurd
),因为从错误的命题来看,任何事情都会随之而来。
我写了一篇关于Conduit背后的想法的博客文章,其中有一个使用absurd
的例子。
答案 3 :(得分:13)
通常,您可以使用它来避免明显部分模式匹配。例如,从this answer获取数据类型声明的近似值:
data RuleSet a = Known !a | Unknown String
data GoRuleChoices = Japanese | Chinese
type LinesOfActionChoices = Void
type GoRuleSet = RuleSet GoRuleChoices
type LinesOfActionRuleSet = RuleSet LinesOfActionChoices
然后您可以像这样使用absurd
,例如:
handleLOARules :: (String -> a) -> LinesOfActionsRuleSet -> a
handleLOARules f r = case r of
Known a -> absurd a
Unknown s -> f s
答案 4 :(得分:11)
如何表达the empty data type有多种方法。一种是空代数数据类型。另一种方法是使其成为∀α.α或
的别名type Void' = forall a . a
在Haskell中的 - 这就是我们如何在System F中编码它(参见Proofs and Types的第11章)。这两个描述当然是同构的,\x -> x :: (forall a.a) -> Void
和absurd :: Void -> a
见证了同构。
在某些情况下,我们更喜欢显式变体,通常是在空数据类型出现在函数的参数中,或者更复杂的数据类型中,例如在Data.Conduit中:
type Sink i m r = Pipe i i Void () m r
在某些情况下,我们更喜欢多态变体,通常空函数类型涉及函数的返回类型。
当我们在这两种表示之间进行转换时,会出现 absurd
。
例如,callcc :: ((a -> m b) -> m a) -> m a
使用(隐式)forall b
。它也可以是类型((a -> m Void) -> m a) -> m a
,因为对控制的调用实际上不会返回,它会将控制转移到另一个点。如果我们想使用continuation,我们可以定义
type Continuation r a = a -> Cont r Void
(我们可以使用type Continuation' r a = forall b . a -> Cont r b
,但这需要排名2类型。)然后,vacuousM
将此Cont r Void
转换为Cont r b
。
(另请注意,您可以使用haskellers.com搜索特定包的使用情况(反向依赖关系),例如查看使用 void 包的人和方式。)
答案 5 :(得分:0)
在像Idris这样的依赖类型语言中,它可能比在Haskell中更有用。通常,在模式匹配实际上无法推入函数的值的总函数中,您将构造一个无人居住类型的值,并使用absurd
来完成案例定义。
例如,此函数从列表中删除一个元素,该元素具有它所在的类型级costraint:
shrink : (xs : Vect (S n) a) -> Elem x xs -> Vect n a
shrink (x :: ys) Here = ys
shrink (y :: []) (There p) = absurd p
shrink (y :: (x :: xs)) (There p) = y :: shrink (x :: xs) p
第二种情况是说空列表中有某个元素,这很荒谬。但是,一般情况下,编译器不知道这一点,我们通常必须明确。然后编译器可以检查函数定义是否不是部分的,并且我们获得了更强的编译时保证。
通过库里 - 霍华德的观点,命题在哪里,那么absurd
就像是一个矛盾证据中的QED。