【问题标题】:Why are instances matched only by their heads?为什么实例只匹配头部?
【发布时间】:2015-07-26 18:20:59
【问题描述】:

我将首先介绍一个具体问题(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”,而不是“所有Wrapper 都是Monad”。

同样,您不能定义 Monad a => Applicative aApplicative a => Functor a 实例。

你不能做的另一件事(这可能只是相关的,我真的不知道)是让一个类成为另一个类的超类,并提供子类的默认实现。当然,class Applicative a => Monad a 很好,但在定义Monad 实例之前,我仍然需要定义Applicative 实例。

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

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

补充: 我怀疑其中一个答案可能类似于“如果我的类型是Wrapper,但我不想使用这暗示的Monad 实例怎么办?”。对此我要问,为什么编译器不能选择最具体的一个?如果有instance Monad MyType,肯定比instance Wrapper a => Monad a 更具体。

【问题讨论】:

  • unwrap 描述了一种Monad 的方法都没有实现的行为;如果不是这种情况,unwrap 将允许您从IO a 计算中提取a(就像unsafePerformIO 所做的那样),这首先会破坏拥有IO monad 的目的.一个类只能不如其子类强大/富有表现力。
  • 当为Maybe 类型定义实例时,Nothingunwrap 的定义是什么?
  • @Sibi 简单,Maybe 没有实例。我不认为他声称有。另外,我怀疑 Wrapper 暗示 Monad 没有任何法律(除了免费法律)。
  • @Jubobs 尽管如此,Luka 的实际主张(即任何可以作为Wrapper 的实例并满足一些明显规律的东西也可以以机械方式成为Monad 的实例) 是正确的。
  • github.com/Frege/frege 中可以通过为子类提供实例来一次实现所有超类。这在子类具有超类函数的默认实现的情况下特别有用,这也是受支持的。 OTOH,Frege 仅支持单参数类型类,如 Haskell 2010。

标签: haskell monads typeclass functor applicative


【解决方案1】:

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

首先:为什么编译器在选择使用哪个实例时不查看实例上下文?这是为了保持实例搜索的效率。如果您要求编译器仅考虑满足其实例头的实例,那么您实际上最终需要您的编译器在所有可能的实例中进行回溯搜索,此时您已经实现了 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

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

【讨论】:

  • 感谢您的回答。一切都说得通。值得注意的是,从 GHC 7.10 开始,OverlappingInstancesIncoherentInstances 已被弃用,取而代之的是每个实例的 pragma deflarations downloads.haskell.org/~ghc/latest/docs/html/users_guide/…
  • 嘿,我才意识到。要编写默认签名,Applicative 需要了解Monad,但由于ApplicativeMonad 的超类,Monad 需要了解Applicative。这是否意味着您只能在所有类型类都在同一个文件中时才能使用DefaultSignatures
  • @LukaHorvat Haskell 报告明确允许递归导入。 (但是,如果您尝试使用此功能,实现通常需要您跳过一些额外的障碍;例如,GHC 需要 .hs-boot 文件来中断任何导入周期。)
【解决方案2】:

一致性和单独编译。

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

-- File: Foo.hs

instance Monad m => Applicative m
instance            Applicative Foo

那么要么这是为Foo 生成Applicative 实例的有效代码,要么是为Foo 生成两个不同的Applicative 实例的错误。它是哪一个取决于 Foo 是否存在 monad 实例。这是个问题,因为很难保证在编译这个模块时知道Monad Foo 是否成立。

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

为此,我们需要知道存在于最终编译程序中的所有实例在每个模块中都是可见的,这导致每个模块都是每个其他模块的依赖项的结论无论模块是否实际导入其他模块。您必须走很长的路才能要求对整个程序进行分析以支持这样的系统,这使得分发预编译库变得困难甚至不可能。

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

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

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

因此,通过忽略约束,我们确保编译器可以对实例做出正确的决定,即使只知道它导入的其他模块(传递性);它不必知道在每个其他模块中定义的所有内容,就可以知道哪些约束成立。

【讨论】:

  • 感谢您的回答。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-12-18
  • 1970-01-01
  • 2012-01-10
  • 1970-01-01
  • 2010-12-20
相关资源
最近更新 更多