我正在Haskell中尝试传递类型类实例。众所周知,不能在原始类型类(即(C a b, C b c) => C a c
(中声明可传递实例。因此,我尝试定义另一个类来表示原始类的传递闭包。最小代码如下:
{-# 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
其中CCD_ 2是CCD_ 3的传递闭包。然而,当我试图在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文档,如果有一个适用的实例,则类型类解析将成功。
为什么会发生这种情况,有什么方法可以解决传递实例问题吗?
我认为您有点误解了文档。他们实际上说,如果存在一个实例,则给定类型的类型类解析将成功。但在你的情况下,没有给出类型。b0
不明确。
编译器需要知道b0
,然后才能选择Coe Bool b0
的实例,即使当前作用域中只有一个实例。这样做是有目的的。其中的关键词是";当前范围";。您可以看到,如果编译器可以选择范围中可用的任何内容,那么您的程序将很容易受到范围中细微变化的影响:您可能会更改导入,或者某些导入的模块可能会更改其内部结构。这可能会导致不同的实例在您当前的范围内出现或消失,这可能会在没有任何警告的情况下导致程序的不同行为。
如果你真的想在任何两种类型之间最多有一条明确的路径,你可以通过向Coe
:添加函数依赖来解决它
class Coe a b | a -> b where
from :: a -> b
这将产生两种影响:
- 编译器会知道,只要知道
a
,它总是可以推导出b
- 为了方便起见,编译器将禁止定义具有相同
a
但不同b
的多个实例
现在编译器可以看到,由于from'
的自变量是Bool
,它必须为某些b
搜索Coe Bool b
的实例,从那里它将确定b
必须是什么,从那里可以搜索下一个实例,依此类推
另一方面,如果你真的打算在两个给定的类型之间有多个可能的路径,而编译器只选择一个,那你就倒霉了。编译器原则上拒绝从多种可能性中随机选择一种——参见上面的解释。