之前使用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
实例适用于任何输入和输出,a
和b
,仅受到转换的两个附加约束的限制函数必须存在于a -> Foo
和Foo -> b
。convertFoo :: a -> Foo
的调用似乎正在挑选convertFoo
本身的定义,因为它是唯一可用的定义,反正。convertFoo
无限地调用自身,因此该函数进入一个永不终止的无限循环。convertFoo
永远不会终止,因此该定义的类型是最低的,因此技术上没有违反任何类型,以及程序类型检查。现在,即使上述理解是正确的,我仍然对为什么整个程序的攻击感到困惑。具体来说,我希望ConvertFoo a Foo
和ConvertFoo Foo b
约束失败,因为不存在这样的实例。
我 理解(至少模糊地)选择一个实例时约束无关紧要 - 仅根据实例头选择实例,然后检查约束 - 所以我可以看到由于我的ConvertFoo a b
实例,这些约束可能会解决得很好,这个实例尽可能宽松。然而,这将需要解决相同的约束集合,我认为这会将类型检查器置于无限循环中,导致GHC挂起编译或给我一个堆栈溢出错误(后者我见过前)。
显然,类型检查器不循环,因为它很高兴地完成并快速编译我的代码。为什么?在这个特定的例子中,实例上下文是如何满足的?为什么这不会给我一个类型错误或产生类型检查循环?
答案 0 :(得分:20)
我完全同意这是一个很好的问题。它说的是如何 我们对类型的直觉与现实不同。
要看看这里发生了什么,就要加大赌注
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)。ConvertFoo Int Foo
(2)和ConvertFoo Foo String
(3)。ConvertFoo Int Foo
(我们已经计算过)和ConvertFoo Foo Foo
(4)。ConvertFoo Foo Foo
(已计算)和ConvertFoo Foo String
(已计算)。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
我们得到了一堆相互递归的实例定义。在这种情况下,这些将在运行时循环,但在编译时没有理由拒绝它们。