Haskell 子类化和实例重叠

Haskell subclassing and instance overlap

来自 OOP 世界,我有时发现自己尝试使用 Haskell 中的继承模式,并取得了不同程度的成功。这是我在子类化(使用 GHC 8.10.7)时遇到的一个小难题。


{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE UndecidableInstances #-}

import Data.List (sort)

class Collection c a where
    -- gets list of elements in the collection
    elements :: c a -> [a]

class OrderedCollection c a where
    -- gets sorted list of elements in the collection
    orderedElements :: c a -> [a]

instance (Ord a, OrderedCollection c a) => Collection c a where
    -- "default" implementation
    elements = orderedElements

newtype SortedList a = SortedList [a]
    deriving Show

instance (Ord a) => OrderedCollection SortedList a where
    -- need to sort the elements in the list
    orderedElements (SortedList xs) = sort xs

instance Collection SortedList a where
    -- "optimized" implementation: no need to sort
    elements (SortedList xs) = xs

test :: (Ord a, Show a, OrderedCollection c a) => c a -> IO ()
test coll = do
    putStrLn $ "ordered elements: " ++ show (orderedElements coll)
    putStrLn $ "elements:         " ++ show (elements coll)

myList :: SortedList Int
myList = SortedList [3, 2, 1]

main :: IO ()
main = do
    test myList

包括必要的语言扩展后,这仍然给我一个错误:Overlapping instances for Collection c a arising from a use of ‘elements’。它建议使用 IncoherentInstances。由于此扩展现已弃用,取而代之的是每个实例的编译指示,我向子类实例添加了一个 INCOHERENT 编译指示:

instance {-# INCOHERENT #-} (Ord a, OrderedCollection c a) => Collection c a where
...

编译成功。然而,结果不是我所期望的,因为输出是:

ordered elements: [1,2,3]
elements:         [1,2,3]

我想要的是 SortedListCollection 的专门实现来覆盖默认值(在 OO 语言中,SortedList 将从 OrderedCollection 继承,然后覆盖 elements 方法)。但是这里类型检查器不知道使用 SortedList 的自定义 Collection 实现,因为 test 的类型签名只强加约束 OrderedCollection c a.

接下来,我尝试添加 Collection 约束:

test :: (Ord a, Show a, Collection c a, OrderedCollection c a) => c a -> IO ()

这给了我想要的输出:

ordered elements: [1,2,3]
elements:         [3,2,1]

但是,GHC 还发出了关于“脆弱的内部绑定”的警告,并建议我添加 MonoLocalBinds 扩展,这样可以消除该警告。无论如何,我对必须包含 Collection c a 约束(鉴于 OrderedCollection c a 隐含)或必须使用不连贯的实例并不感到兴奋。

有趣的是,如果我将 INCOHERENT pragma 更改为 OVERLAPPABLE,它仍然可以编译,并且还允许我删除 MonoLocalBinds.

我的问题是,是否有任何其他方法可以在此处实现所需的“继承”行为,而不需要 test 中的冗余约束?

当你这样写的时候:

instance ... => Collection c a where

您正在为 所有类型 声明一个 Collection 实例。粗箭头 => 左边的内容根本无关紧要。约束不参与实例解析。当编译器尝试查找特定类型的实例时,它只会查看粗箭头 => 右侧的内容,只有在找到匹配的实例后,它才会检查其约束是否得到满足。如果不是,编译器将不会返回寻找另一个实例。这就是实例解析的工作原理,并且有充分的理由。

所以,重申一下:Collection c a 意味着这是 所有类型 .

的实例

因此,您可能声明的任何后续 Collection 个实例当然会重叠。


值得庆幸的是,在这种特殊情况下,有一个更好的方法:您可以声明默认方法而无需创建这样的通用实例。为此,请在 class 声明中声明该方法。是的,您也可以对其施加约束(参见 docs):

class Collection c a where
    -- gets list of elements in the collection
    elements :: c a -> [a]
    default elements :: OrderedCollection c a => c a -> [a]
    elements = orderedElements

但更一般地说,虽然类型 classes 加上存在量化在技术上等同于 OOP-style class 层次结构,但如果您尝试像那样实际建模您的域,它将是你走得越远,就越尴尬和痛苦。这有点像在 Java 中尝试对 ADT 进行建模。技术上可行,但是太乱了!

在一些合理的情况下,class 层次结构可能有意义(一个值得注意的例子是 GHC 异常系统),但大多数时候有很多更简单的方法。