类型类默认方法实例化中的不明确类型解析



为什么下面的代码无法键入?

{-# 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时发生的情况:没有足够的类型信息供编译器了解get调用或change调用的abc。哦,请记住:这两个调用可能来自不同的实例。没有什么可说的,它们必须与changeAll本身来自同一实例。


有两种可能的方法可以解决此问题。

首先,你可以使用函数依赖,这是一种说法,为了确定c,知道ab就足够了:

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

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

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

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

请记住,对于您的changeAll函数,即使是这三个功能依赖项也是不够的,因为changeAll的实现"吞噬"b。也就是说,当它调用get时,唯一已知的类型是a。同样,当它调用change时,唯一已知的类型是c。这意味着,为了使这种b的"吞咽"起作用,它必须仅通过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

最新更新