MultiParamTypeClasses - 为什么这个类型变量是模糊的?

5
假设我定义了一个多参数类型类
{-# LANGUAGE MultiParamTypeClasses, AllowAmbiguousTypes, FlexibleContexts, FlexibleInstances #-}

class Table a b c where
  decrement :: a -> a
  evalutate :: a -> b -> c

然后我定义一个使用 decrement 的函数,为了简单起见:

d = decrement

当我尝试在ghci(版本8.6.3)中加载此内容时:
• Could not deduce (Table a b0 c0)
    arising from a use of ‘decrement’
  from the context: Table a b c
    bound by the type signature for:
               d :: forall a b c. Table a b c => a -> a
    at Thing.hs:13:1-28
  The type variables ‘b0’, ‘c0’ are ambiguous
  Relevant bindings include d :: a -> a (bound at Thing.hs:14:1)
  These potential instance exist:
    instance Table (DummyTable a b) a b

这让我感到困惑,因为d的类型恰好是decrement的类型,在类声明中有说明。

我想到了以下解决方法:

data Table a b = Table (a -> b) ((Table a b) -> (Table a b))

但是这种表示方式在符号上不太方便,而且我只是想知道为什么我首先会收到这个错误信息。


3
这是 GHC 错误信息带来危害的一个例子。你原先的类 Table 无法编译。GHC 给出了一条信息,建议使用 AllowAmbiguousTypes。所以我看到你开启了它。这是个可怕的想法,GHC 不应该提到它。根据 @typedfern 的回答,功能依赖是一个更好的方法。我正在努力修复 GHC,防止它给新手做出荒谬的建议。 - AntC
1
@AntC 我既同意又不同意。一般来说,如果你准备使用 TypeApplications 来消除歧义,那么 AllowAmbiguousTypes 就完全没问题了。我更喜欢这种方法而不是使用代理。但是,在这种特定情况下,它不是正确的解决方案,GHC 的建议也不合适。在这里,应该使用 fundeps(或类型族)。 - chi
1
知道自己在做什么/计划使用TypeApplications的人可能已经打开了AllowAmbiguousTypes。因此,该消息应适用于在其方法签名中犯错误的人。它应提到所有可能的方法/更正,而不是支持一个高级功能(它实际上没有命名),并保持沉默。这就像太空人接管鸡舍一样。 - AntC
1个回答

8
问题在于,由于`decrement`只需要`a`类型,所以无法确定`b`和`c`应该是哪些类型,即使在函数被调用的时候(这样就将多态解决成了一个特定的类型) - 因此,GHC 无法决定使用哪个实例。
例如:假设您有两个 `Table` 实例:`Table Int String Bool` 和 `Table Int Bool Float`; 您在一个期望将Int映射到另一个Int的上下文中调用您的函数 `d` - 问题是,这会匹配两个实例!(`a` 对于两者都是Int)。
请注意,如果将您的函数等同于`evaluate`:
d = evalutate

若编译器接受该代码,则说明由于evaluate依赖于三个类型参数a、b、c,因此在调用地点的上下文中允许非歧义实例分辨 - 只需检查其被调用时,a、b和c所代表的类型即可。

当然,对于单参数类型类,这通常不是问题 - 只有一个类型需要分辨;而涉及多个参数时,情况就会变得复杂...

一种常见的解决方案是使用函数依赖 - 使bc依赖于a

 class Table a b c | a -> b c where
  decrement :: a -> a
  evalutate :: a -> b -> c

这告诉编译器,针对给定类型a的每个Table实例,将只有一个(通过a唯一确定bc); 因此,它将知道不会有任何歧义并欣然接受您的d = decrement

1
如果您无法使用FunctionalDependencies(因为您需要支持相同的a和不同的b和/或c的两个不同的Table a b c),则可以将decrement拆分到单独的类中,并且具有class Decrement a => Table a b c where evaluate :: a -> b -> c。这意味着每个共享aTable必须以相同的方式“减量”其a,但它们仍然可以以不同的方式进行评估,与class Table a b c | a -> b c不同,其中如果两个Table实例共享一个a,则它们实际上必须是相同的实例。 - Ben

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接