GHC类型检查器接受UndecidableInstance技巧的原因及现象解析
问题背景
以下代码可被GHC正常编译:
{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE UndecidableInstances #-} class Blib b where blib :: b class Blob b where blob :: b instance Blib b => Blob b where blob = blib instance Blob b => Blib b where blib = blob bad :: () bad = blib () 4 'a' -- 可添加任意数量、任意类型的参数,代码仍能通过编译 main ::IO () main = return bad
同时存在以下特殊现象:
- 运行编译生成的可执行文件时,程序能“正常”结束;
- 在GHCi中加载代码后,输入
bad会导致GHCi挂起; - 若移除
blib和blob的具体实现(如下所示),GHCi不再挂起:
instance Blib b => Blob b where -- blob = blib instance Blob b => Blib b where -- blib = blob
请问这背后的原理是什么?
原理解析
1. 无限循环的类型实例推导
先看bad的定义:blib () 4 'a'最终要得到()类型。Haskell函数是柯里化的,所以blib的类型会被推断为() -> Int -> Char -> ()——哪怕你加更多参数,GHC都能推断出对应的b类型。
这是因为Blib和Blob的实例定义形成了循环依赖:只要能证明Blib b存在,就能推导出Blob b;反过来只要Blob b存在,又能推导出Blib b。在UndecidableInstances扩展的允许下,GHC的类型检查器会接受这种循环推导,认为任意类型b都能满足Blib和Blob的约束,所以不管给blib加多少参数都能编译通过。
2. 可执行文件正常运行的原因
编译成可执行文件时,GHC的两个机制发挥了作用:
- 惰性求值:只有当值被实际使用时才会触发计算;
- 死代码消除:
main函数只是用return把bad包装成IO动作,但整个程序根本没有用到bad的具体值,所以编译器直接删掉了bad的计算逻辑,不会触发无限递归,程序自然能正常退出。
3. GHCi挂起的原因
在GHCi中输入bad时,GHCi需要计算出bad的实际值并打印输出。此时会触发blib的实现逻辑:blib = blob,而blob又等于blib,形成了无限递归调用——永远跳不出这个循环,最终导致GHCi挂起。
4. 注释实现后不再挂起的原因
如果把blib和blob的实现注释掉,这两个类的实例就没有了具体的方法代码。当GHCi尝试计算bad时,会直接抛出“未定义类方法”的错误,根本不会进入递归调用流程,因此不会挂起。
内容的提问来源于stack exchange,提问作者141592653
相关产品推荐
相关产品推荐

