Data.Void中荒谬的功能有什么用?


97

中的absurd函数Data.Void具有以下签名,其中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中使用示例,但是如果有人想使用依赖类型的语言,我不会抱怨...


5
快速搜索显示,absurd本文已使用此函数处理Contmonad:haskellforall.com/2012/12/the-continuation-monad.html
Artyom

6
你可以看到absurd作为之间的同构的一个方向Voidforall a. a
Daniel Wagner 2013年

Answers:


61

由于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

这真的永远不会产生。这意味着a的正确折叠规则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

也就是说,a Continuation是一个永不返回的函数。 Continuation是“ not”的类型版本。由此我们得到一个CPS monad(对应于经典逻辑)

newtype CPS a = Continuation (Continuation a)

由于Haskell是纯净的,因此我们无法从中获得任何好处。


1
呵呵,实际上我可以稍微讲一点CPS。我以前肯定听说过Curry-Howard双重否定/ CPS对应,但不了解。我不会声称我现在完全明白了,但这肯定会有所帮助!
路易斯·卡西利亚斯

“生活有些艰辛,因为Haskell并不严格 ”-您的确切含义是什么?
埃里克·卡普伦

4
用严格的语言@ErikAllik Void是无人居住的。在Haskell中,它包含_|_。用严格的语言来说,Void永远不能应用带有类型参数的数据构造函数,因此模式匹配的右侧是不可访问的。在Haskell中,您需要使用a !来强制执行该操作,GHC可能不会注意到该路径不可达。
dfeuer 2015年

阿格达呢?它很懒,但是有_|_吗?那它有同样的局限吗?
埃里克·卡普伦

一般而言,agda是总计,因此无法观察到评估顺序。还有,除非你关闭终止检查或类似的东西空型没有闭合AGDA项
菲利普JF

58

考虑由自由变量参数化的lambda项的这种表示形式。(参见Bellegarde和Hook的论文1994,Bird和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

我只是将lambda表示为闭包。通过将自由变量映射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很少单独使用它,但是在您想要以某种方式表示某种不可能的方式实例化类型参数时非常方便(例如,在此处使用封闭变量free)。通常,这些参数化类型在高阶函数的参数提升操作的操作对整个类型(例如,在这里,fmap>>=eval)。因此,您可以通过absurd进行通用操作Void

再举一个例子,假设使用Either e v捕获希望为您提供计算v但可能会引发type异常的计算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)

实际上,也许是相关的。如果您喜欢冒险,这篇未完成的文章将介绍如何使用Void自由变量压缩术语的表示形式

data Term f x = Var x | Con (f (Term f x))   -- the Free monad, yet again

从a DifferentiableTraversablefunctor 自由生成的任何语法中f。我们Term f Void用来表示没有自由变量的区域,并[D f (Term f Void)]用来表示通过没有自由变量的区域隧穿到孤立的自由变量或到两个或多个自由变量的路径中的结点的管道。必须在某个时间完成那篇文章。

对于没有值的类型(或至少没有一个值得礼貌的公司说),Void它非常有用。以及absurd如何使用它。


forall f. vacuous f = unsafeCoerce f是有效的GHC重写规则吗?
仙人掌

1
@仙人掌,不是真的。虚假Functor实例可能是GADT,实际上与函子不同。
dfeuer

那些Functor不会违反fmap id = id规则吗?还是您所说的“虚假”是什么意思?
仙人掌

35

我认为这在某些情况下可能是有用的,因为它是详尽处理“不可能发生”的情况的类型安全方式

这是完全正确的。

您可以说这absurd没有什么比const (error "Impossible")。但是,它是类型限制的,因此它的唯一输入可以是type Void,一种有意无人居住的数据类型。这意味着您无法传递任何实际值absurd。如果您最终进入类型检查器认为您可以访问类型的代码的代码分支Void,那么您就处于一种荒谬的境地。因此,您只是absurd用来基本上标记该代码分支永远都不会到达。

“ Ex falso quodlibet”的字面意思是“从[一个]错误的[主张]开始,一切都会随之而来”。因此,当您发现自己持有的数据类型为时Void,就知道手中有错误的证据。因此,您可以(通过)填补任何想要的空缺absurd,因为从一个错误的命题开始,一切都会随之而来。

我写了一篇有关Conduit背后想法的博客文章,其中包含一个使用示例absurd

http://unknownparallel.wordpress.com/2012/07/30/pipes-to-conduits-part-6-leftovers/#running-a-pipeline


13

通常,您可以使用它来避免明显的部分模式匹配。例如,从以下答案中获取数据类型声明的近似值:

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

13

有多种方法表示空数据类型。一种是空的代数数据类型。另一种方法是使其成为∀α.α

type Void' = forall a . a

在Haskell中-这是我们如何在System F中对其进行编码(请参见证明和类型的第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。它也可以是type ((a -> m Void) -> m a) -> m a,因为对终止的调用实际上不会返回,而是将控制转移到另一点。如果我们想使用延续,我们可以定义

type Continuation r a = a -> Cont r Void

(我们可以使用,type Continuation' r a = forall b . a -> Cont r b但需要等级2类型。)然后vacuousM将其转换Cont r VoidCont r b

(还请注意,您可以使用haskellers.com来搜索特定程序包的用法(反向依赖性),例如查看谁以及如何使用void程序包。)


TypeApplications可以用来更明确地说明proof :: (forall a. a) -> Voidproof fls = fls @Void
Iceland_jack '16

1

在像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在矛盾证明中的一种。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.