为什么下面的代码无法键入?
{-# 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 ~ Int
和b ~ String
,那么选择哪个呢?㗵。信息不足。模糊。
这正是当你尝试在map change . get
中调用get
和change
时发生的情况:没有足够的类型信息供编译器了解get
调用或change
调用的a
、b
和c
。哦,请记住:这两个调用可能来自不同的实例。没有什么可说的,它们必须与changeAll
本身来自同一实例。
有两种可能的方法可以解决此问题。
首先,你可以使用函数依赖,这是一种说法,为了确定c
,知道a
和b
就足够了:
class C a b c | a b -> c where ...
如果以这种方式声明类,编译器将拒绝相同a
和b
的多个实例,但c
不同,另一方面,它将能够通过知道a
和b
来选择实例。
当然,您可以在同一类上有多个功能依赖项。例如,您可以声明知道任意两个变量就足以确定第三个变量:
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
(注意:为了能够像这样引用函数体中的类型变量a
、b
和c
,还需要启用ScopedTypeVariables
(
当然,在调用changeAll
本身时,您还需要执行此操作,因为它的类型签名中也没有足够的信息:
foo = changeAll @Int @String @Bool 42