在使用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
具体来说,convertFoo
当给定任何输入以产生任何输出时,函数会进行类型检查,如evil
函数所示。起初,我以为我可能不小心实现了unsafeCoerce
,但这并不完全正确:实际上尝试调用我的convertFoo
函数(例如,通过执行类似的操作evil 3
)只是进入了无限循环。
我有点模糊地理解发生了什么。我对这个问题的理解是这样的:
ConvertFoo
我定义的实例适用于任何输入和输出,a
并且b
仅受转换函数必须存在的两个附加约束的限制a -> Foo
和Foo -> b
。- 不知何故,该定义能够匹配任何输入和输出类型,因此似乎调用 to
convertFoo :: a -> Foo
正在选择convertFoo
自身的定义,因为无论如何它是唯一可用的定义。 - 由于
convertFoo
无限调用自身,函数进入一个永不终止的无限循环。 - 由于
convertFoo
永不终止,该定义的类型是底部,所以从技术上讲,没有任何类型被违反,并且程序类型检查。
现在,即使上面的理解是正确的,我仍然对为什么整个程序进行类型检查感到困惑。具体来说,鉴于不存在此类实例,我预计ConvertFoo a Foo
and约束会失败。ConvertFoo Foo b
我确实(至少模糊地)理解在选择实例时约束并不重要 - 仅根据实例头选择实例,然后检查约束 - 所以我可以看到这些约束可能会因为我的ConvertFoo a b
实例而很好地解决,这是尽可能宽容的。但是,这将需要解决相同的约束集,我认为这会将类型检查器置于无限循环中,导致 GHC 要么挂起编译,要么给我一个堆栈溢出错误(后者我见过前)。
不过,很明显,类型检查器不会循环,因为它会愉快地触底并愉快地编译我的代码。为什么?在这个特定示例中,实例上下文如何得到满足?为什么这不会给我一个类型错误或产生一个类型检查循环?