类型类默认方法实例化中的模糊类型解析

Ambiguous type resolution in typeclass default method instantiations

为什么下面的代码打不出来?

{-# LANGUAGE AllowAmbiguousTypes, MultiParamTypeClasses #-}

module Main where

class Interface a b c where
  get :: a -> [b]
  change :: b -> c

  changeAll :: a -> [c]
  changeAll = map change . get

main = return ()

如果我注释掉 --changeAll = map change . get 的默认实例化,一切似乎都很好。但是,在实例化到位后,我收到此错误:

GHCi, version 8.6.5: http://www.haskell.org/ghc/  :? for help
[1 of 1] Compiling Main             ( test.hs, interpreted )

test.hs:10:19: error:
    • Could not deduce (Interface a0 b0 c)
        arising from a use of ‘change’
      from the context: Interface a b c
        bound by the class declaration for ‘Interface’ at test.hs:5:7-15
      The type variables ‘a0’, ‘b0’ are ambiguous
      Relevant bindings include
        changeAll :: a -> [c] (bound at test.hs:10:3)
    • In the first argument of ‘map’, namely ‘change’
      In the first argument of ‘(.)’, namely ‘map change’
      In the expression: map change . get
   |
10 |   changeAll = map change . get
   |                   ^^^^^^

test.hs:10:28: error:
    • Could not deduce (Interface a b0 c0) arising from a use of ‘get’
      from the context: Interface a b c
        bound by the class declaration for ‘Interface’ at test.hs:5:7-15
      The type variables ‘b0’, ‘c0’ are ambiguous
      Relevant bindings include
        changeAll :: a -> [c] (bound at test.hs:10:3)
    • In the second argument of ‘(.)’, namely ‘get’
      In the expression: map change . get
      In an equation for ‘changeAll’: changeAll = map change . get
   |
10 |   changeAll = map change . get
   |                            ^^^

我是不是漏掉了什么明显的东西?

你所有的方法都是有歧义的类型。

为了更好地说明问题,让我们将示例简化为一种方法:

class C a b c where
    get :: a -> [b]

现在假设您有以下实例:

instance C Int String Bool where
    get x = [show x]

instance C Int String Char where
    get x = ["foo"]

然后假设您正在尝试调用方法:

s :: [String]
s = get (42 :: Int)

s的签名,编译器知道b ~ String。从get的参数,编译器知道a ~ Int。但是 c 是什么?编译器不知道。无处可寻。

但是等等! C 的两个实例都匹配 a ~ Intb ~ String,那么选择哪个呢?不清楚。没有足够的信息。暧昧。

这正是您尝试在 map change . get 中调用 getchange 时发生的情况:编译器没有足够的类型信息来理解 abc 用于 get 调用或 change 调用。哦,请记住:这两个调用可能来自不同的实例。没有什么可说的,它们必须来自与 changeAll 本身相同的实例。


有两种可能的方法来解决这个问题。

首先,你可能会用到一个函数依赖,就是说为了确定c,知道[=30就足够了] =] 和 b:

class C a b c | a b -> c where ...

如果您以这种方式声明 class,编译器将拒绝相同 ab 但不同 c 的多个实例,反之, 它将能够通过知道 ab.

来选择实例

当然,您可以在同一个 class 上拥有多个功能依赖项。例如,您可以声明知道任何两个变量应该足以确定第三个变量:

class C a b c | a b -> c, a c -> b, b c -> a where ...

请记住,对于您的 changeAll 函数,即使这三个函数依赖项也不够,因为 changeAll "swallows" b 的实现。也就是说,当它调用 get 时,唯一已知的类型是 a。同样,当它调用 change 时,唯一已知的类型是 c。这意味着,为了使 b 的 "swallowing" 起作用,它必须可以单独由 a 以及单独由 c 确定:

class Interface a b c | a -> b, c -> b where ...

当然,这只有在您的程序逻辑确实具有这种属性某些变量由其他变量确定的情况下才有可能。如果您真的需要所有变量都是独立的,请继续阅读。


其次,您可以通过使用 TypeApplications:

显式告诉编译器必须是什么类型
 s :: String
 s = get @Int @String @Bool 42  -- works

没有更多的歧义。编译器确切地知道选择哪个实例,因为你已经明确地告诉它。

将此应用于您的 changeAll 实施:

changeAll :: a -> [c]
changeAll = map (change @a @b @c) . get @a @b @c

(注意:为了能够像这样在函数体中引用类型变量 abc,您还需要启用 ScopedTypeVariables)

当然你也需要在调用 changeAll 本身时这样做,因为它的类型签名中也没有足够的信息:

foo = changeAll @Int @String @Bool 42