在 Haskell 中,即使启用了 AllowAmbiguousTypes,为什么类型仍然不明确?

In Haskell, why are types ambiguous even with AllowAmbiguousTypes enabled?

所以假设您有两个类型 类 定义如下:

{-# LANGUAGE MultiParamTypeClasses #-}

class F a c where f :: a -> c
class G c b where g :: c -> b

然后您想使用 f 和 g 以通用方式定义一个新函数 h。

h a = g (f a)

我们知道这个函数的类型是 a -> b 所以 c 是隐式的。我想把它留给 fg 的实施者,c 可能是什么。 Haskell抱怨这句话c含糊不清。

然后按照错误提示我开启了这个扩展:

{-# LANGUAGE AllowAmbiguousTypes #-}

现在可以了!不错

我相信通常作为一种良好的软件工程实践,我想编写我的函数的明确规范,以告诉编译器我期望我的函数应该有什么样的行为。这样以后的编译器就可以抱怨我不尊重我的设置。

所以我想在它之前添加我的函数类型:

h :: (F a c, G c b) => a -> b
h a = g (f a)

现在类型歧义错误又来了...为什么?

总结为什么 Haskell 抱怨下面这段代码? 即使显式启用了 AllowAmbiguousTypes。如何在保持显式函数类型定义的同时修复它?我知道删除函数的类型定义可以解决问题,但我不想指定不足。

{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE AllowAmbiguousTypes #-}

class F a c where f :: a -> c
class G c b where g :: c -> b

h :: (F a c, G c b) => a -> b
h a = g (f a)

为什么 Haskell 不抱怨这个?

{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE AllowAmbiguousTypes #-}

class F a c where f :: a -> c
class G c b where g :: c -> b

h a = g (f a)

错误信息:

error:
    * Could not deduce (G c0 b) arising from a use of `g'
      from the context: (F a c, G c b)
        bound by the type signature for:
                   h :: forall a c b. (F a c, G c b) => a -> b

      The type variable `c0' is ambiguous
      Relevant bindings include
        h :: a -> b 
    * In the expression: g (f a)
      In an equation for `h': h a = g (f a)
    |
    | h a = g (f a)
    |       ^^^^^^^
error:
    * Could not deduce (F a c0) arising from a use of `f'
      from the context: (F a c, G c b)
        bound by the type signature for:
                   h :: forall a c b. (F a c, G c b) => a -> b
      The type variable `c0' is ambiguous
      Relevant bindings include
        a :: a 
        h :: a -> b 
    * In the first argument of `g', namely `(f a)'
      In the expression: g (f a)
      In an equation for `h': h a = g (f a)
    |
    | h a = g (f a)
    |          ^^^

您的代码含糊不清,编译器无法自动解决。假设这样:

class F a c where f :: a -> c
class G c b where g :: c -> b

instance F Int String where f = show
instance G String Bool where g = null

h :: (F Int c, G c Bool) => Int -> Bool
h a = g (f a)

现在,最后一行使用了哪些实例?我们有两个选择:使用上下文 (F Int c, G c Bool) 提供的实例,或者忽略该上下文并使用上面的实例,其中 String 作为中间类型。两种解释都是正确的,我们确实可以明确地写

h1 :: forall c. (F Int c, G c Bool) => Int -> Bool
h1 a = (g :: c -> Bool) (f a)

h2 :: forall c. (F Int c, G c Bool) => Int -> Bool
h2 a = (g :: String -> Bool) (f a)

选择一种方式。 GHC无法以合理的方式为我们做这个选择。它可以根据一些试探法选择一个,但这会给程序员带来很大的惊喜。因此,我们可以争辩说 GHC 绝对不能 选择,报告歧义,并让程序员阐明他们的意图。

最后,请注意,您的代码不包含上述两个实例这一事实是无关紧要的,因为它们可以稍后添加,甚至可以添加到另一个模块中,因此 GHC 必须保守并避免假设它们永远不会存在。

And why doesn't Haskell complain about this?

{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE AllowAmbiguousTypes #-}

class F a c where f :: a -> c
class G c b where g :: c -> b

h x = g (f x)  -- [renamed to x for clarity]

说得好。这里GHC可以找到一个最通用的类​​型,就是

h :: forall a b c. (F a c, G c b) => a -> b
h x = (g :: c -> b) ((f :: a -> b) x)

由于这里是GHC添加了类型变量c,GHC可以确定这样的类型就是中间类型。毕竟,该类型变量是在类型推断期间创建的,用于表示中间类型。

相反,当用户显式写入上下文时,GHC 无法猜测用户的意图。有可能,即使在实践中不太可能,用户不想使用该实例,而是另一个实例(在程序中可用,但不存在于上下文中)。

考虑一下这个案例可能也会有所帮助:

data T = ....

h :: forall a b c. (F a c, G c b, F a T, G T b) => a -> b
h x = g (f x)

我认为您同意应该拒绝此代码:中间类型可能是 Tc,并且没有解决它的明智方法。现在考虑这种情况:

h :: forall a b c. (F a c, G c b) => a -> b
h x = g (f x)

instance F a T where ...
instance G T b where ...

现在,这与之前的案例没有太大区别。我们没有在上下文中有两个选项,而是将一个选项移到了外面。不过,GHC 仍然有两个选项可供选择。所以,再一次,明智的做法是拒绝代码,并向程序员询问更多细节。


一个更简单的场景,在 GHCi 中:

> :set -XScopedTypeVariables
> :set -XAllowAmbiguousTypes
> class C a where c :: String
> instance C Int where c = "Int"
> instance C Bool where c = "Bool"
> let g :: forall a. C a => String ; g = c

<interactive>:7:40: error:
    * Could not deduce (C a0) arising from a use of `c'

在这里,GHC 怎么知道我写 g = c 的意思是 "the c coming from the context C a? I could have written that meaning " 来自 Int" 实例的 cBool .

GHC 在内部生成一个新类型变量 a0,然后尝试求解约束 C a0。它有三个选择:选择 a0 = aa0 = Inta0 = Bool。 (以后可以添加更多实例!)

所以,它是模棱两可的,如果不猜测程序员的意图,就没有理智的方法来修复它。唯一安全的选择是拒绝。