为什么这段代码使用UndecidableInstances编译,然后生成运行时无限循环?

时间:2016-05-23 22:37:59

标签: haskell typeclass undecidable-instances

之前使用UndecidableInstances编写一些代码时,我遇到了一些我发现非常奇怪的东西。当我认为它不应该时,我设法无意中创建了一些类型的代码:

{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE UndecidableInstances #-}

data Foo = Foo

class ConvertFoo a b where
  convertFoo :: a -> b

instance (ConvertFoo a Foo, ConvertFoo Foo b) => ConvertFoo a b where
  convertFoo = convertFoo . (convertFoo :: a -> Foo)

evil :: Int -> String
evil = convertFoo

具体来说,当{em>任何输入生成任何输出时,convertFoo函数类型检查,如evil函数所示。起初,我想也许我设法意外地实现了unsafeCoerce,但事实并非如此:实际上是试图调用我的convertFoo函数(例如通过执行类似evil 3的操作)简单地进入一个无限循环。

有点从模糊的意义上理解正在发生的事情。我对这个问题的理解是这样的:

  • 我定义的ConvertFoo实例适用于任何输入和输出,ab,仅受到转换的两个附加约束的限制函数必须存在于a -> FooFoo -> b
  • 不知何故,该定义能够匹配任何输入和输出类型,因此对convertFoo :: a -> Foo的调用似乎正在挑选convertFoo本身的定义,因为它是唯一可用的定义,反正。
  • 由于convertFoo无限地调用自身,因此该函数进入一个永不终止的无限循环。
  • 由于convertFoo永远不会终止,因此该定义的类型是最低的,因此技术上没有违反任何类型,以及程序类型检查。

现在,即使上述理解是正确的,我仍然对为什么整个程序的攻击感到困惑。具体来说,我希望ConvertFoo a FooConvertFoo Foo b约束失败,因为不存在这样的实例。

理解(至少模糊地)选择一个实例时约束无关紧要 - 仅根据实例头选择实例,然后检查约束 - 所以我可以看到由于我的ConvertFoo a b实例,这些约束可能会解决得很好,这个实例尽可能宽松。然而,这将需要解决相同的约束集合,我认为这会将类型检查器置于无限循环中,导致GHC挂起编译或给我一个堆栈溢出错误(后者我见过前)。

显然,类型检查器循环,因为它很高兴地完成并快速编译我的代码。为什么?在这个特定的例子中,实例上下文是如何满足的?为什么这不会给我一个类型错误或产生类型检查循环?

2 个答案:

答案 0 :(得分:20)

我完全同意这是一个很好的问题。它说的是如何 我们对类型的直觉与现实不同。

Typeclass惊喜

要看看这里发生了什么,就要加大赌注 evil的类型签名:

data X

class Convert a b where
  convert :: a -> b

instance (Convert a X, Convert X b) => Convert a b where
  convert = convert . (convert :: a -> X)

evil :: a -> b
evil = convert

显然,正在选择Covert a b实例,因为只有。{1}}实例 这个类的一个实例。 typechecker正在考虑类似的事情 这样:

  • Convert a X如果......
    • Convert a X是真的[假设] [/ li>
    • Convert X X是真的
      • Convert X X如果......
        • Convert X X是真的[假设] [/ li>
        • Convert X X是真的[假设] [/ li>
  • Convert X b如果......
    • Convert X X是真的[从上面开始]
    • Convert X b为真[假设为真]

typechecker让我们感到惊讶。我们不期望Convert X X 是的,因为我们没有定义类似的东西。但(Convert X X, Convert X X) => Convert X X是一种重言式:它是 自动为true,无论在类中定义了什么方法都是如此。

这可能与我们的类型类的心理模型不匹配。我们期待 编译器在这一点上傻笑并抱怨Convert X X 不可能是真的,因为我们没有为它定义任何实例。我们期待 编译器站在Convert X X,寻找另一个位置 走到Convert X X为真的地方,并因为那里而放弃 没有其他地方这是真的。但编译器能够 递归!递归,循环,并且图灵完成。

我们用这种能力祝福了类型搜索者,我们做到了 UndecidableInstances。当文档说明它是 有可能将编译器发送到循环中很容易假设 最坏的,我们假设坏循环总是无限循环。但 在这里,我们演示了一个更加致命的循环,一个循环 终止 - 除了令人惊讶的方式。

(丹尼尔的评论更加明显地证明了这一点:

class Loop a where
  loop :: a

instance Loop a => Loop a where
  loop = loop

不确定性的危险

这是UndecidableInstances的确切情况 允许。如果我们关闭该扩展程序并启用FlexibleContexts (一种无害的扩展,只是语法本质上),我们得到一个 警告违反Paterson conditions之一:

...
Constraint is no smaller than the instance head
  in the constraint: Convert a X
(Use UndecidableInstances to permit this)
In the instance declaration for ‘Convert a b’

...
Constraint is no smaller than the instance head
  in the constraint: Convert X b
(Use UndecidableInstances to permit this)
In the instance declaration for ‘Convert a b’

"不小于实例头,"虽然我们可以在心理上重写它 as"这个实例可能用于证明一个断言 它本身就会让你痛苦不堪,咬牙切齿和打字。"该 帕特森条件一起防止实例解析中的循环。 我们的违规行为说明了为什么它们是必要的,我们可以 可能会咨询一些论文,看看为什么它们就足够了。

打底

至于为什么程序在运行时无限循环:有无聊 回答,evil :: a -> b不能无限循环或抛出一个 异常或通常触底,因为我们信任Haskell typechecker并没有可以居住a -> b的价值,除了 底部。

更有趣的答案是,因为Convert X X是 在实际上,它的实例定义就是这个无限循环

convertXX :: X -> X
convertXX = convertXX . convertXX

我们可以类似地扩展Convert A B实例定义。

convertAB :: A -> B
convertAB =
  convertXB . convertAX
  where
    convertAX = convertXX . convertAX
    convertXX = convertXX . convertXX
    convertXB = convertXB . convertXX

这种令人惊讶的行为,以及如何约束实例解析(通过 默认没有扩展名)是为了避免这些 行为,也许可以作为Haskell的原因 类型类系统尚未得到广泛采用。尽管如此 令人印象深刻的人气和力量,它有奇怪的角落(无论是 它在文档或错误消息或语法中,甚至可能在 它的基本逻辑)似乎特别适合我们的人类 考虑类型级抽象。

答案 1 :(得分:7)

以下是我在精神上处理这些案件的方法:

class ConvertFoo a b where convertFoo :: a -> b
instance (ConvertFoo a Foo, ConvertFoo Foo b) => ConvertFoo a b where
  convertFoo = ...

evil :: Int -> String
evil = convertFoo

首先,我们从计算所需实例集开始。

  • evil直接需要ConvertFoo Int String(1)。
  • 然后,(1)需要ConvertFoo Int Foo(2)和ConvertFoo Foo String(3)。
  • 然后,(2)需要ConvertFoo Int Foo(我们已经计算过)和ConvertFoo Foo Foo(4)。
  • 然后(3)需要ConvertFoo Foo Foo(已计算)和ConvertFoo Foo String(已计算)。
  • 然后(4)需要ConvertFoo Foo Foo(已计算)和ConvertFoo Foo Foo(已计算)。

因此,我们到达一个固定点,这是一组有限的必需实例。编译器在有限时间内计算设置没有问题:只需应用实例定义,直到不再需要约束为止。

然后,我们继续为这些实例提供代码。在这里。

convertFoo_1 :: Int -> String
convertFoo_1 = convertFoo_3 . convertFoo_2
convertFoo_2 :: Int -> Foo
convertFoo_2 = convertFoo_4 . convertFoo_2
convertFoo_3 :: Foo -> String
convertFoo_3 = convertFoo_3 . convertFoo_4
convertFoo_4 :: Foo -> Foo
convertFoo_4 = convertFoo_4 . convertFoo_4

我们得到了一堆相互递归的实例定义。在这种情况下,这些将在运行时循环,但在编译时没有理由拒绝它们。