如何定义可重叠的依赖实例?

How to define overlappable depending instances?

所以我有一个类型class有点像这样

class Stringify x where
  stringify :: x -> String

我还有另外两种类型class有点像那些

class LTextify x where
  ltextify :: x -> L.Text
class Textify x where
  textify :: x -> T.Text

我想定义依赖类型classes

class (LTextify x) => Stringify x where
  stringify = L.unpack . ltextify
class (Textify x) => Stringify x where
  stringify = T.unpack . textify

但是 haskell 当然会抱怨“重复的实例声明”,这当然是真的。我试过玩 OVERLAPPABLE 和 OVERLAPPING,但无济于事。我想要的是,如果 class 有一个 LTextify 实例,那么应该使用它。如果不是,那么如果它有一个 Textify 的实例,那么应该使用它来实现 Stringify。我希望 class 也能够明确定义 Stringify 的实例,然后与可能存在的 Textify 或 LTextify 实例重叠。当然是确定性的方式。

我假设最后两个应该是实例声明而不是 class 声明?

instance (LTextify x) => Stringify x where ...
instance (Textify x) => Stringify x where ...

根据实例是否存在或 Haskell 是否真正支持,具有不同的行为,因为孤立实例意味着可用实例列表可能会根据导入以不可预测的方式发生变化。我建议(如果可能的话)让 Stringify 成为 TextifyLTextify 的超级 class。

此外,这些特定的实例将无法创建,因为 GHC 实例解析的第一步是丢弃所有约束并检查哪个实例在结构上匹配,因此它看起来像这样:

instance Stringify x
instance Stringify x

根本无法区分这两者,所以 ghc 将不得不在其中任意选择一个,并希望它是正确的。

为了使用 OVERLAPPING pragmas,需要有一个实例比另一个更具体,比如

instance C a => Foo [a]
instance {-# OVERLAPPING #-} Foo [Char]

请参阅用户手册中的 this section 以更深入地了解其工作原理。这是相关的引用:

GHC requires that it be unambiguous which instance declaration should be used to resolve a type-class constraint. GHC also provides a way to loosen the instance resolution, by allowing more than one instance to match, provided there is a most specific one.


还有一个已弃用的扩展 IncoherentInstances,它允许多个匹配的实例,即使一个实例并不比另一个更具体,但即使是这样也不能在这里使用,因为两个实例在结构上是相同的。

IncoherentInstances 将任意选择其中一个实例,因此应尽可能避免,并且仅在匹配哪个实例无关紧要时才使用。


编辑:定义实例时减少样板的一种方法是将DerivingVia与新类型包装器一起使用:

newtype UsingLTextify a = UsingLTextify { unwrapLT :: a }
newtype UsingTextify  a = UsingTextify  { unwrapT  :: a }

instance LTextify a => Stringify (UsingLTextify a) where
  stringify = L.unpack . ltextify . unwrapLT
instance Textify a  => Stringify (UsingTextify a) where
  stringify = T.unpack . textify . unwrapT

然后你可以用它来派生这样的实例

{-# LANGUAGE DerivingVia #-}

data Foo = ...
  deriving Stringify via UsingLTextify Foo
instance LTextify Foo where ...

如果您无法控制数据类型并且不想添加孤立实例(您应该尽可能避免),也可以直接使用这些新类型,方法是将它们包装在您的数据周围以赋予它相关实例。