Haskell 中的类型类解析报告含糊不清,即使只有一个实例

Typeclass resolution in Haskell reporting ambiguity even if there is only one instance

我正在 Haskell 中试验传递类型 class 实例。众所周知,不能在原始类型class(即(C a b, C b c) => C a c)中声明一个传递实例。因此,我尝试定义另一个 class 来表示原始 class 的传递闭包。最小代码如下:

{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
module Ambig where

class Coe a b where
  from :: a -> b

class CoeTrans a b where
  from' :: a -> b

instance CoeTrans a a where
  from' = id

instance (Coe a b, CoeTrans b c) => CoeTrans a c where
  from' = from' . from @a @b

instance Coe Bool Int where
  from False = 0
  from True = 1

instance Coe Int Integer where
  from x = toInteger x

其中 CoeTransCoe 的传递闭包。但是,当我尝试在 CoeTrans 中使用 from' 时,它总是报告有歧义:

-- >>> from' True :: Integer
-- Ambiguous type variable ‘b0’ arising from a use of ‘from'’
-- prevents the constraint ‘(Coe Bool b0)’ from being solved.
-- Probable fix: use a type annotation to specify what ‘b0’ should be.
-- These potential instance exist:
--   instance Coe Bool Int
--     -- Defined at /Users/t/Desktop/aqn/src/Ambig.hs:21:10

即使实际上只有一个实例。但根据 GHC docs 类型class 解决方案将成功,前提是存在一个适用的实例。

为什么会出现这种情况,有什么办法可以解决传递实例问题吗?

我认为您对文档有一点误解。他们真的说,如果存在一个实例,给定类型 的类型类解析 将会成功。但在你的情况下,没有给出类型。 b0 有歧义。

编译器在选择 Coe Bool b0 的实例之前需要知道 b0,即使当前作用域中只有一个实例。这是故意这样做的。那里的关键词是“当前范围”。你看,如果编译器可以只选择范围内可用的任何内容,你的程序将容易受到范围内细微变化的影响:你可能会更改你的导入,或者你的某些导入模块可能会更改它们的内部结构。这可能会导致不同的实例在您当前的范围内出现或消失,这可能会导致您的程序在没有任何警告的情况下出现不同的行为。


如果你真的打算在任何两种类型之间总是最多只有一条明确的路径,你可以通过向Coe添加函数依赖来解决它:

class Coe a b | a -> b where
  from :: a -> b

这将产生两个影响:

  1. 编译器知道它总是可以通过知道 a 来推断 b
  2. 并且为了促进这一点,编译器将禁止定义具有相同 a 但不同 b 的多个实例。

现在编译器可以看到,由于 from' 的参数是 Bool,它必须为某些 b 搜索 Coe Bool b 的实例,并且从在那里它将确定 b 必须是什么,然后它可以从那里搜索下一个实例,依此类推。


另一方面,如果您真的打算在两个给定类型之间存在多个可能的路径,并且让编译器只选择一个 - 您真不走运。编译器原则上拒绝随机选择多种可能性中的一种 - 请参阅上面的解释。