【问题标题】:How does the type inference algorithm resolve guarded equations?类型推断算法如何解决受保护的方程?
【发布时间】:2025-11-30 04:20:02
【问题描述】:

我试图找出 Haskell 是如何从一个没有通过网络指定类型的受保护方程中解析类型的。

例如遵循函数定义,ghci 能够解析类型并准确告诉我它是什么。

fun a b c
| a == c = b
| b < 0 = a+b
| otherwise = c

它是如何做到的?我知道这对于 if-then-else 结构是如何工作的(基本上:从通用版本开始,添加约束)但我想知道这里需要哪些额外步骤?

【问题讨论】:

  • 守卫有点像 if/then/else,至少在概念上是这样,所以我不确定你为什么认为这里需要解释一些额外的东西。
  • 守卫实际上可以简单理解为if-表达式的语法糖(又可以简化为case,这也允许模式守卫)。 GHC 实际上在所有脱糖之前进行类型检查,但这是一个实现细节,主要是为了获得更好的错误消息。

标签: haskell


【解决方案1】:

fun 有三个参数和一个结果。所以最初编译器假设它们可能是不同的类型:

fun :: alpha -> beta -> gamma -> delta         -- the type vars are placeholders

忽略守卫,看看方程的 rhs 结果:它们每个都必须是类型delta。所以从词项级方程中建立一系列类型级方程。在类型之间使用~ 表示它们必须相同。

  • 第一个方程有结果b,所以b的类型必须与结果相同,即beta ~ delta
  • 第三个方程有结果c,所以c的类型必须与结果gamma ~ delta相同。
  • 第二个等式有 rhs + 运算符 :: Num a =&gt; a -&gt; a -&gt; a。我们将回到Num a =&gt;。这就是说+ 有两个参数和一个结果,它们都是相同的类型。该类型是 fun 的 rhs 的结果,因此必须是 delta+ 的第二个参数是b,所以b 的类型必须与结果beta ~ delta 相同。我们已经从第一个等式中得到了这一点,所以这只是为了确认一致性。
  • + 的第一个参数是a,所以a 的类型必须再次相同,alpha ~ delta

我们有alpha ~ beta ~ gamma ~ delta。所以回到最初的签名(尽可能通用),用equals代替equals:

fun :: (constraints) => alpha -> alpha -> alpha -> alpha

约束

从运营商那里即时取货。

  • 由于运营商+,我们已经看到Num
  • 第一个等式的a == c 为我们提供Eq
  • 第二个等式的b &lt; 0 为我们提供Ord
  • 实际上0 的出现给了我们另一个Num,因为0 :: Num a =&gt; a&lt; :: Ord a =&gt; a -&gt; a -&gt; Bool IOW 的参数&lt; 必须是相同的类型和相同的约束。

所以在fun的类型前面堆积这些约束,消除重复

fun :: (Num alpha, Eq alpha, Ord alpha) => alpha -> alpha -> alpha -> alpha

这就是你最喜欢的编译器告诉你的吗?它可能使用类型变量a 而不是alpha。它可能没有显示Eq alpha

消除不必要的超类约束

Main> :i Ord
...
class Eq a => Ord a where
...

Eq a =&gt; 告诉每个Ord 类型必须已经带有Eq。因此,在为fun 提供签名时,编译器会假设它的Eq 消失。

fun :: (Num a, Ord a) => a -> a -> a -> a

QED

【讨论】:

  • 感谢您提供详细的 QED!这有助于我更好地理解类型推断在 Haskell 中的工作原理。我假设根据类型推断隐式处理守卫和 if/then/else 构造。
【解决方案2】:
fun a b c
| a == c = b
| b < 0 = a+b
| otherwise = c

是否清楚这如何转换为 if/else?

fun a b c
 = if a == c
      then b
      else if b < 0
             then a+b
             else c

如果我们添加一些人类可读的注释:

fun a b c      -- Start reasoning with types of `a`, `b`, `c`, and `result :: r`
 = if a == c   -- types `a` and `c` are equal,  aka `a ~ c`.  Also `Eq a, Eq c`.
      then b   -- types `b ~ r`
      else if b < 0   -- `Num b, Ord b`
             then a+b -- `a ~ b ~ r` and `Num a, Num b`
             else c   -- `c ~ r`

如果你把所有这些事实放在一起,这个类型很快就会归结起来。

a ~ b ~ r  and c ~ r

所以我们知道实际上只有一种类型,我们只需调用 a 并在事实中重命名所有其他类型变量。

Num a, Eq a, Ord a

我们知道Ord 意味着Eq 是一个小的认知节省,所以我们最终可以得到约束Num a, Ord a

所有这些都席卷了机制 - 利用诸如 (==) t1 t2 ~~&gt; t1 = t2 之类的含义 - 在地毯下,但希望以可接受的方式。

【讨论】:

  • 看看你的工作,我宁愿认为if-then-else 的嵌套使得更难看到程序的逻辑或其类型推断。这尤其适用于存在类型错误的情况:编译器将指向保护源代码行中的错误,而不是去糖版本。
  • @AntC 我不明白你在说什么。提问者提到他们知道面对 if-then-else 的类型推断是如何工作的,所以我只是试图展示守卫如何不需要任何额外的类型检查知识。我并不是提倡在实际程序中替换守卫,也不是作为一个幼稚的编译步骤。
最近更新 更多