You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.20 22:13:23