想象一下,我们有一个使用库的 Haskell 程序。该程序从其依赖项之一为类型 T 提供类型类 TC 实例。在同一库的下一个版本中,库作者为类型 T 提供了类型类 TC 的另一个实例。
我们想使用这两个类型类实例。我们怎么能这样做?
PS newtype 解决方案不起作用。这两个实例都驻留在我们无法控制的库中。
PPS 我没有真实代码的例子。这是一个理论问题。我只想了解类型类如何与库可组合性一起工作。
Haskell 2010 报告§4.3.2指出
- 一个类型不能在程序中多次声明为特定类的实例。
所以在标准的 Haskell 中是不可能的。
我不知道有任何 GHC 扩展允许您在 GHC 中执行此操作。
这是(一个?)为什么孤立实例(在与类型和类型类不同的模块中定义实例)通常被认为是一个坏主意的原因。
一般来说,同一类型的多个类型类实例是不可能的。但是,如果一个类型在不同的包中定义,或者是同一包的旧版本,ghc 会认为它是不同的类型。所以理论上你可以为它定义和实例,并为它foo-1.1
定义和实例,并将两者一起使用。Foo
foo-1.2
Foo
然而,在实践中,这不会很好地工作。函数仅适用于一种类型或另一种类型。当您编写一个对 a 进行操作的函数时Foo
,它将仅对一个特定的 进行操作Foo
,而不是两者。本质上,您有两种完全独立的、无可争议的类型。使用起来会很尴尬,与构建的尴尬程度完全不同。
我可以肯定地说,使用 Cabal 你只能依赖同一个库的一个版本。
虽然您至少可以将实例的替代版本的源代码复制粘贴到您的项目中,但您仍然只能在每个模块中导入其中一个。如果您将冲突的实例导入模块,您将遇到无法确定的实例问题,您将没有实际的解决方案。
说实话,我无法想象为什么要为同一个库发出的同一种类型拥有同一个类的不同实例。这似乎非常不切实际。尽管我对可能对您的情况有所帮助的猜测是有两个具有相应实例的类型类:一个来自库的当前版本,另一个 - 旧版本的重命名源代码副本。
如果您想要更好的灵活性和可组合性,请将类型类具体化为记录。RankNTypes
可能是必要的。
例如,以下是如何具体化 Applicative 类型类
{-# LANGUAGE RankNTypes #-}
data ApplicativeInstance f = ApplicativeInstance
{ pure :: forall a. a -> f a
, amap :: forall a b. (a -> b) -> f a -> f b
, ap :: forall a b. f (a -> b) -> f a -> f b
}
listApplicative = ApplicativeInstance
{ pure = \a -> [a]
, amap = map
, ap = \fs xs -> case fs of
[] -> []
f:fs' -> map f xs ++ ap cartesianListApplicative fs' xs
}
zipListApplicative = ApplicativeInstance
{ pure = \a -> [a]
, amap = map
, ap = \fs xs -> case (fs, xs) of
([], _) -> []
(_, []) -> []
(f:fs', x:xs') -> f x : ap zipListApplicative fs' xs'
}
现在,我们获得了指定我们想要的实例的能力。然而,我们失去了隐式选择实例的能力:选择现在必须是显式的。
ghci> ap listApplicative [(+1), (*3)] [1 .. 5]
[2,3,4,5,6,3,6,9,12,15]
ghci> ap zip
zip zipListApplicative zipWith3
zip3 zipWith
ghci> ap zipListApplicative [(+1), (*3)] [1 .. 5]
[2,6]
另见: http: //lukepalmer.wordpress.com/2010/01/24/haskell-antipattern-existential-typeclass/
在依赖树上链接同一个包的不同版本,现在是不可能的,因为它会导致依赖冲突。
但这在未来是可能的,正如这个GHC 状态更新视频中所述,只要我猜它们在同一个库中只使用一个版本。