类型类默认方法实例化中的歧义类型解析

and*_*wja 3 haskell types type-inference typeclass

为什么以下代码无法键入?

{-# 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 ()
Run Code Online (Sandbox Code Playgroud)

如果我注释掉 的默认实例化--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
   |                            ^^^
Run Code Online (Sandbox Code Playgroud)

我在这里遗漏了一些明显的东西吗?

Fyo*_*kin 9

你所有的方法都是模糊类型的。

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

class C a b c where
    get :: a -> [b]
Run Code Online (Sandbox Code Playgroud)

现在假设您有以下实例:

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

instance C Int String Char where
    get x = ["foo"]
Run Code Online (Sandbox Code Playgroud)

然后想象您正在尝试调用该方法:

s :: [String]
s = get (42 :: Int)
Run Code Online (Sandbox Code Playgroud)

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

可是等等!Cmatcha ~ Int和 的两个实例b ~ String,那么选择哪个?不清楚。没有足够的信息。模糊的。

这就是当你想呼叫发生了什么get,并changemap change . get:没有足够的类型信息,编译器理解ab以及c对于无论是get来电或change通话。哦,请记住:这两个调用可能来自不同的实例。没有什么可以说它们必须与changeAll自身来自同一个实例。


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

首先,您可以使用函数依赖,这是一种说法,为了确定c,知道a和就足够了b

class C a b c | a b -> c where ...
Run Code Online (Sandbox Code Playgroud)

如果以这种方式声明类,编译器将拒绝相同ab但不同的多个实例,c另一方面,它可以仅通过知道aand来选择实例b

当然,你可以对同一个类有多个函数依赖。例如,您可以声明知道任何两个变量应该足以确定第三个变量:

class C a b c | a b -> c, a c -> b, b c -> a where ...
Run Code Online (Sandbox Code Playgroud)

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

class Interface a b c | a -> b, c -> b where ...
Run Code Online (Sandbox Code Playgroud)

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


其次,你可以明确地告诉编译器是什么类型必须使用TypeApplications

 s :: String
 s = get @Int @String @Bool 42  -- works
Run Code Online (Sandbox Code Playgroud)

不再有歧义。编译器确切地知道要选择哪个实例,因为您已经明确地告诉了它。

将此应用于您的changeAll实现:

changeAll :: a -> [c]
changeAll = map (change @a @b @c) . get @a @b @c
Run Code Online (Sandbox Code Playgroud)

(注:为了能够引用类型变量ab以及c在函数体中那样,你还需要启用ScopedTypeVariables

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

foo = changeAll @Int @String @Bool 42
Run Code Online (Sandbox Code Playgroud)

  • 完美的。我只会补充一点,当推断出某些类型时,您不需要传递所有类型。例如,“get @_ @_ @Bool”应该在第一个示例中起作用,因为“@Int”和“@String”是推断出来的。 (2认同)