Haskell 如何处理多参数typeclass的函数,谁不需要typeclass的每种类型?
我定义了一个typeclass,它类似于一个接口,其中包含我的程序所需的一系列函数。不幸的是,它需要多个多态类型,但不是这个多参数类型类的每个函数都需要每个类型。GHC的不可还原类型困扰着我,我无法运行代码 一个简化的例子:Haskell 如何处理多参数typeclass的函数,谁不需要typeclass的每种类型?,haskell,Haskell,我定义了一个typeclass,它类似于一个接口,其中包含我的程序所需的一系列函数。不幸的是,它需要多个多态类型,但不是这个多参数类型类的每个函数都需要每个类型。GHC的不可还原类型困扰着我,我无法运行代码 一个简化的例子: {-# LANGUAGE MultiParamTypeClasses #-} class Foo a b where -- ... bar :: a -> () baz :: Foo a b => a -> () baz = bar
{-# LANGUAGE MultiParamTypeClasses #-}
class Foo a b where
-- ...
bar :: a -> ()
baz :: Foo a b => a -> ()
baz = bar
GHC说
可能的修复:添加修复这些类型变量的类型签名
如何为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
使用该定义(需要函数依赖性
扩展名),您告诉编译器对于a
的任何特定选择,只有一个有效的b
选择。甚至试图定义上述两个实例都将是一个编译错误
鉴于此,编译器知道它可以仅基于类型
a
选择Foo
的实例来使用。在这种情况下,可以调用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
(因此不能像其他人提到的那样使用函数依赖项)使用多个实例,一种可能对您来说是正确的,也可能不正确的方法是使用标记有“幻影”类型的新类型,该类型仅用于指导类型选择。这包括:
{-# 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
然后,您将能够以这样一种方式包装您的值,即您可以提供一个显式类型注释来选择正确的实例。当多参数类型类变得笨拙时,重构它们的另一种方法是使用扩展。与此类似,当您可以将类重新构造为只有一个参数(或至少更少的参数)时,这会很好地工作,而实例之间不同的其他类型是从实际的类参数计算出来的 通常我发现,每当我认为我需要一个多参数类型的类时,参数几乎总是一起变化,而不是独立变化。在这种情况下,选择一个作为“主要”并使用一些系统从中确定其他系统要容易得多。函数依赖项和类型族一样可以做到这一点,但许多人发现类型族更容易理解 下面是一个例子:
{-# 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
最后,我编写了一个函数来展示当您在使用的
Glue
实例上进行多态时如何使用Glued
;基本上,你在你的类型签名中“调用”Glued
;这意味着gluebotways
不“知道”粘贴的a是什么类型,但它知道它如何对应于a
。如果你知道你在粘字符,但不想硬编码,Glued Char=String
这是否意味着如果我想要类似的功能,我需要将这些函数放入更小的合适的类型类中,然后让更高的类型类依赖于它?如果你试图将它们都合并到一个超类中,你会遇到同样的问题。老实说,听起来你好像过度使用了课程。有可能你不是,但至少同样有可能有一个更简单的方法。嗯,我不知道。我经常使用类型类来抽象IO,这样它就可以依赖于IO之前的另一层。我不应该这样做吗?@Vektorweg:Control.Monad.IO.Class和liftIO
为此有什么问题?如果您使用异常,可能会使用monad控件
或异常
)进行扩充。重构您的类型类以避免不依赖于整个上下文的函数。无论如何,多参数类型类都是相当笨拙的——如果可以避免,就这样做。只有在定义了类似于instance Foo Int b
的内容时,这才可能起作用,在这种情况下,编译器将能够仅基于第一个参数选择一个实例。如果是这种情况,您可以简单地使用Fo
而不是Foo
,因为您的所有实例都是相同的。
{-# 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)