我正在设计一个库,它将极大地受益于 OverlappingInstances 编译器标志的使用。但是每个人都在谈论这个扩展并警告它的危险。我的问题是,在 hackage 的任何地方都有很好地使用这个扩展的例子吗?关于如何封装坏处并正确使用扩展是否有任何经验法则?
3 回答
也许一个思想实验会让这个扩展有点神秘。
假设我们已经放弃了使用多个模式案例定义的函数必须全部在一个地方的限制,这样您就可以foo ("bar", Nothing) = ...
在模块的顶部编写,然后在foo ("baz", Just x) = ...
其他地方有案例。事实上,让我们更进一步,允许在不同的模块中完全定义案例!
如果您认为这听起来会令人困惑且容易出错,那么您是对的。
为了恢复一些理智,我们可以添加一些限制。例如 (ha, ha),我们可以要求以下属性保持:
- 在任何使用这种函数的地方,给定的参数必须完全匹配一种模式。其他任何东西都是编译器错误。
- 添加新模式(包括通过导入另一个模块)永远不应改变有效代码的含义——要么选择相同的模式,要么产生编译器错误。
应该清楚的是,匹配简单的构造函数,比如True
orNothing
是很简单的。我们也可以稍微手动一下,并假设编译器可以消除文字的歧义,比如"bar"
和"baz"
以上。
另一方面,用 like 模式绑定参数(x, Just y)
变得尴尬——编写这样的模式意味着放弃编写类似(True, _)
或(False, Just "foobar")
以后的模式的能力,因为这会产生歧义。更糟糕的是,模式保护变得几乎没用,因为它们需要非常普遍的匹配。许多常见的习语会产生无穷无尽的歧义头痛,当然编写一个“默认”的贯穿模式是完全不可能的。
这大致是类型类实例的情况。
我们可以通过放宽所需的属性来重新获得一些表达能力:
- 在任何使用这种函数的地方,它必须至少匹配一种模式。没有匹配是编译器错误。
- 如果使用一个函数以使多个模式匹配,则将使用最具体的模式。如果没有唯一的最特定模式,则会产生错误。
- 如果以与一般实例匹配的方式使用函数,但可以在运行时将其应用于与更具体实例匹配的参数,则这是编译器错误。
请注意,我们现在处于这样一种情况,即仅导入一个模块就可以通过将一个新的、更具体的模式带入范围来改变函数的行为。在涉及高阶函数的复杂情况下,事情也可能变得模糊不清。尽管如此,在许多情况下,问题是不太可能出现的——例如,在库中定义一个通用的失败模式,同时让客户端代码在需要时添加特定的案例。
这就是你的大致位置OverlappingInstances
。正如上面示例中所建议的,如果创建新的重叠总是不可能或不希望的,并且不同的模块最终不会看到不同的、冲突的实例,那么它可能没问题。
真正归结为,OverlappingInstances
在“开放世界”假设下,任何可能的实例都可以在以后添加,因此消除的限制是为了使使用类型类表现得更加明智。通过放宽这些要求,您自己承担了这些负担;因此,请仔细考虑可以添加新实例的所有方式,以及这些场景中的任何一个是否是一个重大问题。如果您确信在晦涩难懂的极端情况下没有什么可以打破,那么请继续使用扩展。
大多数人请求重叠实例是因为他们想要约束导向推理而不是类型导向推理。类型类是为类型导向推理而设计的,而 Haskell 没有为约束导向推理提供优雅的解决方案。
但是,您仍然可以通过使用新类型来“封装优点”。给定以下容易出现重叠实例的实例定义:
instance (SomeConstraint a) => SomeClass a where ...
您可以改为使用:
newtype N a = N { unN :: a }
instance (SomeConstraint a) => SomeClass (N a) where ...
现在 Haskell 的类型类系统有一个适当的特定类型来匹配 (ie N a
),而不是无缘无故地匹配每个单一类型。这使您可以控制实例的范围,因为现在只有包装在N
newtype 中的内容才会匹配。
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
行为(选择第一个匹配的实例)更容易推理和更灵活,因此至少在设计的探索阶段使用它。一旦我有一些东西可以做我想做的事,我就会尝试摆脱扩展,特别注意那些表现不佳的东西(比如这些)。