为什么实例仅与头部相匹配?

时间:2015-05-14 23:26:15

标签: haskell monads typeclass functor applicative

我首先介绍一个具体的问题(StackOverflow这样的人)。 假设你定义一个简单的类型

data T a = T a

此类型为FunctorApplicativeMonad。忽略自动派生,要获取那些必须编写每个实例的实例,即使Monad隐含Applicative,也意味着Functor。 更重要的是,我可以定义一个这样的类

class Wrapper f where
    wrap   :: a -> f a
    unwrap :: f a -> a

这是一个非常强大的条件,它肯定意味着Monad,但我不能写

instance Wrapper f => Monad f where
    return = wrap
    fa >>= f = f $ unwrap fa

因为出于某种原因,这意味着"一切都是Monad(每f},只有当它是Wrapper"而不是" sa Wrapper的所有内容都是Monad"。

同样,您无法定义Monad a => Applicative aApplicative a => Functor a个实例。

你不能做的另一件事(它可能只是相关的,我真的不知道)是有一个类是另一个类的超类,并提供子类的默认实现。当然,class Applicative a => Monad a非常棒,但在定义Applicative实例之前,我仍然需要定义Monad实例。“ / p>

这不是一个咆哮。我写了很多,因为否则这很快会被标记为“过于宽泛”#34;或者"不清楚"。问题归结为标题。 我知道(至少我很确定)这有一些理论上的原因,所以我想知道这里有什么好处。

作为一个子问题,我想问一下,是否有可行的替代方案仍能保留所有(或大部分)优势,但允许我写的内容。

增加: 我怀疑其中一个答案可能就是"如果我的类型是Wrapper怎么办,但我不想使用这意味着的Monad实例?& #34 ;.对此我要问,为什么编译器不能选择最具体的一个呢?如果有instance Monad MyType,则肯定比instance Wrapper a => Monad a更具体。

2 个答案:

答案 0 :(得分:12)

这里有很多问题。但是,让我们一次拿一个。

首先:为什么编译器在选择使用哪个实例时不会查看实例上下文?这是为了保持实例搜索的有效性。如果您要求编译器仅考虑其实例头满足的实例,那么您最终需要编译器在所有可能的实例中进行反向跟踪搜索,此时您已实现了90%的Prolog。另一方面,如果您采取立场(如Haskell所做的那样),在选择要使用的实例时只查看实例头,然后简单地强制实例上下文,则没有回溯:每时每刻都只有你可以做出的一个选择。

下一步:为什么你不能让一个类成为另一个类的超类,并提供子类的默认实现?这种限制没有根本原因,因此GHC提供此功能作为扩展。你可以这样写:

{-# LANGUAGE DefaultSignatures #-}
class Applicative f where
    pure :: a -> f a
    (<*>) :: f (a -> b) -> f a -> f b

    default pure :: Monad f => a -> f a
    default (<*>) :: Monad f => f (a -> b) -> f a -> f b
    pure = return
    (<*>) = ap

然后,一旦您提供了instance Monad M where ...,您就可以简单地编写instance Applicative M而没有where子句,并让它正常工作。我不知道为什么在标准库中没有这样做。

最后:为什么编译器不能允许多个实例,只选择最具体的实例?这个问题的答案有点是前两个问题的结合:有很好的基本原因,这并不能很好地运作,但GHC提供了一个扩展功能。这不能很好地工作的根本原因是在运行时之前不能知道给定值的最具体的实例。 GHC对此的回答是,对于多态值,选择与可用的完整多态性兼容的最具特异性的值。如果以后那件事情变得单一,那么,对你来说太糟糕了。其结果是一些函数可能对一个实例的某些数据进行操作,而其他函数可能对另一个实例的相同数据进行操作;这可能导致非常微妙的错误。如果在所有这些讨论之后你仍然认为这是一个好主意,并且拒绝从别人的错误中吸取教训,那么你可以开启IncoherentInstances

我认为这涵盖了所有问题。

答案 1 :(得分:4)

一致性和单独编译。

如果我们有两个头部匹配但有不同约束的实例,请说:

-- File: Foo.hs

instance Monad m => Applicative m
instance            Applicative Foo

然后,这是为Applicative生成Foo实例的有效代码,或者为Applicative生成两个不同的Foo实例时出错。它是的哪一个取决于Foo 是否存在monad实例。这是一个问题,因为在编译此模块时,很难保证Monad Foo是否成立的知识会在编译器中出现。

另一个模块(比如Bar.hs)可能会为Monad生成Foo个实例。如果Foo.hs没有导入该模块(甚至间接导入),那么编译器如何知道?更糟糕的是,我们可以通过更改以后是否在最终程序中包含Bar.hs更改这是错误还是有效定义!

为了实现这一点,我们需要知道最终编译程序中存在的所有实例在每个模块中都是可见的,这导致每个模块都是每个其他模块的依赖关系无论模块是否实际导入其他。您不得不走很远的路,需要进行全程序分析才能支持这样的系统,这使得分发预编译的库变得难以实现。

避免这种情况的唯一方法是永远不要让GHC根据负面信息做出决定。您无法根据另一个实例的 - 存在来选择实例。

这意味着必须忽略实例上的约束,例如解析。无论约束是否成立,您都需要选择一个实例;如果它留下了多个可能适用的实例,那么你需要负面信息(即除了其中一个之外的所有要求都不需要约束)才能接受代码有效。

如果您只有一个实例甚至是候选者,并且您无法看到其约束的证明,则可以通过将约束传递到使用实例的位置来接受代码(我们可以依赖于将这些信息传递给其他模块,因为他们必须导入这个信息,即使只是间接导入;如果那些位置也无法看到所需的实例,那么他们就会产生关于未满足约束的适当错误。

因此,通过忽略约束,我们确保编译器可以做出关于实例的正确决策,即使只知道它导入的其他模块(传递);它不必了解每个其他模块中定义的所有内容,以便了解所持有的约束。