OverlappingInstances有哪些好的使用案例?

17

我正在设计一个图书馆,它将极大地受益于使用OverlappingInstances编译器标志。 但是每个人都在批评这个扩展并警告其危险性。我的问题是,在hackage上是否有好的使用该扩展的示例?有没有关于如何封装不良行为并正确使用该扩展的经验法则?

3个回答

32

或许通过一个思维实验可以稍微解释一下这种扩展。

假设我们放弃了多重模式定义函数必须都在同一个地方的限制,这样你就可以在模块的顶部写出 foo ("bar", Nothing) = ...,然后在其他地方写出 foo ("baz", Just x) = ... 这样的模式。事实上,让我们更进一步,允许在完全不同的模块中定义模式!

如果你觉得这听起来会让使用变得混乱且容易出错,那么你是正确的。

为了恢复一些理智,我们可以添加一些限制。例如(哈哈),我们可以要求满足以下属性:

  • 在任何使用此类函数的地方,给定的参数必须与正好一个模式匹配。否则会产生编译器错误。
  • 添加新模式(包括通过导入另一个模块)不应改变有效代码的含义——要么选择相同的模式,要么生成编译器错误。

很明显,匹配简单构造函数如TrueNothing非常简单。我们还可以有点含糊其辞,假设编译器可以消除如上所述的字面量歧义,例如"bar""baz"

然而,使用模式绑定参数如(x, Just y)就变得非常棘手——写这样一个模式意味着放弃后续编写 (True, _)(False, Just "foobar") 等模式的能力,因为那会产生歧义。更糟糕的是,模式保护几乎变得无用了,因为它们需要非常通用的匹配。许多常见的惯用语将导致无休止的歧义头痛,当然编写"默认"的落空模式也是完全不可能的。

这大致就是类型类实例的情况。

我们可以通过以下方式放宽所需属性来恢复一些表达能力:

  • 在使用此类函数的任何地方,它必须与至少一个模式匹配。没有任何匹配时会产生编译器错误。
  • 如果使用一个函数时有多个模式匹配,将使用最具体的模式。如果没有唯一的最具体模式,则会产生错误。
  • 如果以一种与一般实例匹配的方式使用函数,但在运行时可以将其应用于将匹配更具体实例的参数,则这是编译器错误。

请注意,我们现在处于这样一种情况:仅导入一个模块就可以通过引入一个新的、更具体的模式来改变函数的行为。在涉及高阶函数的复杂情况下,事情可能会变得混乱。尽管如此,在许多情况下,问题不太可能出现——例如,在库中定义一个通用的 fall-through 模式,同时让客户端代码添加必要的特定情况。

这大致就是 OverlappingInstances 所能提供的了。正如上面的示例所建议的那样,如果创建新的重叠总是不可能或者是期望的,并且不同的模块不会看到不同的、相互冲突的实例,那么它可能是没问题的。

真正关键的是,OverlappingInstances 移除的限制是为了使在“开放世界”假设下使用类型类的行为合理化,即任何可能的实例以后都可以添加。通过放松这些要求,您将自行承担这个负担;因此,请仔细考虑新实例的添加方式以及这些情况是否会带来重大问题。如果您确信即使在偏僻和狡猾的边角案例中也不会出现任何问题,那么就继续使用该扩展。


1
这是一个非常棒的答案。 - John L
我同意。那是一个非常好的答案。 - MathematicalOrchid

13

大多数人请求重叠实例是因为他们想要约束导向推理而不是类型导向推理。类型类是为了类型导向推理而设计的,Haskell没有提供优雅的解决方案来进行约束导向推理。

然而,您仍然可以通过使用newtype来“封装好处”。 给定以下容易出现重叠实例的实例定义:

instance (SomeConstraint a) => SomeClass a where ...

您可以使用以下代码替代:

newtype N a = N { unN :: a }

instance (SomeConstraint a) => SomeClass (N a) where ...

现在Haskell的类型类系统有了一个专门的类型来匹配,即N a,而不是随意匹配每一种类型。这使得您可以控制实例的范围,因为只有被N newtype包装的内容才能匹配。


3
缺乏约束导向推理的理论基础存在吗? - Mike Izbicki
3
这是我读过的最清晰简洁的建议,应该在所有教程中都加上它。 - AndrewC
我已经谷歌搜索了“约束导向推理”,但没有找到任何信息。你能详细解释一下这个概念吗? - CMCDragonkai
1
约束导向推理只是我编造的一个术语,因为我不知道它的官方术语是什么。我用它来表示从可用的约束中推断出类型类实例。 - Gabriella Gonzalez
我非常喜欢“约束导向推理”的解释。然而,你的解决方案并没有达到这个目的:在这种情况下,整个问题的关键在于你不知道哪些约束对于你的多态类型是满足的,因此你不可能用正确的newtype来包装它以获得所需的实例。 - crockeea

2

OverlappingInstances可以让你在类型类级别上编写许多有用的代码,否则无法实现。然而,其中绝大部分可以重组以使用单个函数依赖(以此处的种类多态风格编写)。

class TypeEq (a :: k) (b :: k) (t :: Bool) | a b -> t where
  typeEq :: Proxy a -> Proxy b -> HBool t

目前只能使用OverlappingInstance(以完全通用的方式)来实现此功能。使用案例包括Oleg将OOP编码为Haskell。因此,我认为OverlappingInstances的一个好用例是经典HList论文中TypeEq的这个实现

这种特定功能可以非常轻松地通过编译器支持提供(甚至可以在类型函数而不是fundep级别上工作),因此在某个地方粘贴一个带有TypeEq的单个模块对我来说并不那么糟糕。

当我从事危险的类型类黑客工作时,我经常发现IncoherentInstances行为(选择第一个匹配的实例)更易于推理和更灵活,因此至少在设计的探索阶段使用它。一旦我有了想要的东西,我会尝试去除扩展,特别注意那些行为不良的扩展(像这些)。


请注意,TypeEq 可以从非重叠实例中使用,因此确实可以隔离扩展的使用。您不能将其与类型族一起使用,但这是一个不同的问题。顺便说一下,IncoherentInstances 大致意味着放弃我答案中“宽松”属性的第三个。 GHC 不再要求实例选择与实际使用一致,并在出现歧义时抱怨,而是会选择看起来最好的实例。耶! - C. A. McCann

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接