我定义了一个类似于接口的类型类,其中包含我的程序所需的一堆函数。遗憾的是,它需要多个多态类型,但并不是这个多参数类型类的每个函数都需要每种类型。 GHC 用不可推导的类型困扰着我,我无法运行代码。
简化示例:
{-# LANGUAGE MultiParamTypeClasses #-}
class Foo a b where
-- ...
bar :: a -> ()
baz :: Foo a b => a -> ()
baz = bar
GHC 说
Possible fix: add a type signature that fixes these type variable(s)
我该如何为
b
做到这一点?特别是当我想保持 b
多态性时。只有 Foo
的实例才能定义此类型。
这是不可能的。
根本问题是多参数类型类依赖于每个类型参数。如果类中的特定定义没有使用每个类型参数,则编译器将永远无法知道您所指的实例,并且您甚至永远无法指定它。 考虑以下示例:
class Foo a b where
bar :: String -> IO a
instance Foo Int Char where
bar x = return $ read x
instance Foo Int () where
bar x = read <$> readFile x
这两个实例使用它们的参数执行完全不同的操作。编译器选择这些实例之一的唯一方法是匹配两个类型参数。但没有办法指定类型参数是什么。班级简直就是破烂不堪。无法调用
bar
函数,因为您永远无法为编译器提供足够的信息来解析要使用的类实例。
那么为什么类定义没有被编译器拒绝呢?因为有时您可以使用
FunctionalDependencies
扩展使其工作。
如果一个类有多个参数,但它们是相关的,有时可以将该信息添加到类的定义中,以允许类成员不使用类定义中的每个类型变量。
class Foo a b | a -> b where
bar :: String -> IO a
通过该定义(需要
FunctionalDependencies
扩展),您可以告诉编译器,对于 a
的任何特定选择,b
只有一个有效选择。尝试定义上述两个实例都会出现编译错误。
鉴于此,编译器知道它可以仅根据类型
Foo
选择要使用的 a
实例。在这种情况下,可以调用bar
。
将其拆分为较小的类型类可能就足够了。
{-# LANGUAGE MultiParamTypeClasses #-}
class Fo a => Foo a b where
-- ...
foo :: a -> b -> ()
class Fo a where
bar :: a -> ()
baz :: Foo a b => a -> ()
baz = bar
假设您确实想对给定的
a
使用多个实例(因此不能像其他人提到的那样使用函数依赖项),一种可能适合或不适合您的可能性是使用带有 a 的 newtype tagged “幻影”类型仅用于指导类型选择。 这编译:
{-# LANGUAGE MultiParamTypeClasses #-}
newtype Tagged t a = Tagged { unTagged :: a } -- Also defined in the tagged package
-- on Hackage
class Foo a b where
bar :: Tagged b a -> ()
baz :: Foo a b => Tagged b a -> ()
baz = bar
然后,您将能够以可以给出显式类型注释来选择正确实例的方式包装您的值。
当多参数类型类变得尴尬时重构它们的另一种方法是使用 TypeFamilies 扩展。与 FunctionalDependency 一样,当您可以将类重新构造为仅具有单个参数(或至少更少的参数),而实例与实例之间不同的其他类型由实际类参数计算时,这种方法效果很好。
通常,我发现每当我认为需要多参数类型类时,参数几乎总是一起变化而不是独立变化。在这种情况下,选择一个作为“主要”并使用某种系统从中确定其他系统要容易得多。函数依赖和类型族都可以做到这一点,但许多人发现类型族更容易理解。这是一个例子:
{-# LANGUAGE TypeFamilies, FlexibleInstances #-}
class Glue a where
type Glued a
glue :: a -> a -> Glued a
instance Glue Char where
type Glued Char = String
glue x y = [x, y]
instance Glue String where
type Glued String = String
glue x y = x ++ y
glueBothWays :: Glue a => a -> a -> (Glued a, Glued a)
glueBothWays x y = (glue x y, glue y x)
上面声明了一个类
Glue
,其类型可以通过 glue
操作粘合在一起,并且具有相应的类型,即“粘合”的结果。
然后我声明了几个实例;
Glued Char
是 String
,
Glued String
也只是
String
。
最后,我编写了一个函数来展示当您对正在使用的
Glued
实例进行多态处理时如何使用 Glue
;基本上你在类型签名中“调用”
Glued
作为函数;这意味着
glueBothWays
不“知道”
Glued a
是什么类型,但它知道它如何对应于
a
。如果您知道自己正在粘合字符,但又不想对
Glued Char
的假设进行硬编码,您甚至可以使用
Glued Char = String
作为类型。我认为解决这个问题的另一种方法是将不同的
不同的值,而不是类型
。当然,这取决于您还想对代码其余部分中的不同
a
做什么,但在我的用例中,以下翻译有所帮助。
类定义
class Foo a b where
other :: b
bar :: a -> ()
被以下替换,我们调整
baz
的类型:
data Foo b = Foo
{ other :: b
, bar :: () }
baz :: Foo b -> ()
baz = bar
请注意,Foo a b => a -> ()
变成了 Foo b -> ()
。它仍然是多态的
b
。
我们将定义一个值,而不是实例
Foo Int String
inty :: Foo String
inty = Foo { other = "Hello", bar = () }
()
更改为
Either () ()
,我们还可以得到一个示例,其中
baz
又名 bar
的行为实际上取决于 a
:
data Foo b = Foo
{ other :: b
, bar :: Either () () }
baz :: Foo b -> Either () ()
baz = bar
int :: Foo String
int = Foo
{ other = "Hello"
, bar = Left () }
char :: Foo String
char = Foo
{ other = "Bye"
, bar = Right () }
-- >>> baz int
-- Left ()
-- >>> baz char
-- Right ()