Haskell 类型族中的类型歧义

Type ambiguity in Haskell type families

我正在尝试将以下 class Domain 及其实例 TrivialDomain

放在一起
{-# LANGUAGE TypeFamilies #-}

data Transition = Transition

class Domain d where
    type Set d
    type Engine d :: * -> *

    top :: Engine d (Set d)

    -- ...
    complement :: Set d -> Engine d (Set d)
    exclude    :: Set d -> Set d -> Engine d (Set d)
    -- ...

data TrivialDomain = TrivialDomain

instance Domain TrivialDomain where
    type Set TrivialDomain = [Int]
    type Engine TrivialDomain = IO

    top = return [0..10]

    -- ...
    complement a = top >>= (flip exclude) a
    exclude a b  = return $ filter (not . (`elem` b)) a
    -- ...

但我不断收到以下我无法理解的错误

test3.hs:25:21:
    Couldn't match type ‘Engine d0’ with ‘IO’
    The type variable ‘d0’ is ambiguous
    Expected type: IO (Set d0)
      Actual type: Engine d0 (Set d0)
    In the first argument of ‘(>>=)’, namely ‘top’
    In the expression: top >>= (flip exclude) a
test3.hs:25:35:
    Couldn't match type ‘Set d1’ with ‘[Int]’
    The type variable ‘d1’ is ambiguous
    Expected type: Set d0 -> [Int] -> IO [Int]
      Actual type: Set d1 -> Set d1 -> Engine d1 (Set d1)
    In the first argument of ‘flip’, namely ‘exclude’
    In the second argument of ‘(>>=)’, namely ‘(flip exclude) a’

我希望 Engine d (Set d) 在实例声明中解析为 IO [Int],但似乎并非如此。至少GHC不这么认为。我错过了什么?

在您的情况下,关联类型不足以推断方法的类型。

您有 class Domain d,并且 SetEngine 关联到 d。这意味着只要我们的程序中有一个已知的 d 和一个已知的 Domain d 实例,GHC 就可以解析 Set dEngine d。但这不会倒退。 GHC 无法解析存在 Set dEngine ddDomain 实例,因为完全有可能存在不同的 Domain 实例具有相同的 SetEngine 类型。

由于您的 class 方法仅提及 SetEngine,因此永远无法从方法使用中推断出 Domain d

您可以根据自己的目标做几件事。

首先,您可以使 d 依赖于 SetEngine:

class Domain set engine where
  type DomainOf set engine :: *
  -- ...

更一般地说,FunctionalDependencies 使您可以更灵活地强制执行类型之间的依赖关系。比如可以具体声明每个Set只有一个d,这样就可以恢复好的类型推断了:

class Domain d set engine | d -> set engine, set -> d where

    top        :: engine set
    complement :: set -> engine set
    exclude    :: set -> set -> engine set

data TrivialDomain = TrivialDomain

instance Domain TrivialDomain [Int] IO where

    top = return [0..10]

    complement a = top >>= (flip exclude) a

    exclude a b  = return $ filter (not . (`elem` b)) a

最后,如果您想使用原来的 class,您必须将 Proxy d 参数添加到您的方法中,以使实例和关联类型可解析:

import Data.Proxy

data Transition = Transition

class Domain d where
    type Set d
    type Engine d :: * -> *

    top        :: Proxy d -> Engine d (Set d)
    complement :: Proxy d -> Set d -> Engine d (Set d)
    exclude    :: Proxy d -> Set d -> Set d -> Engine d (Set d)

data TrivialDomain = TrivialDomain

instance Domain TrivialDomain where
    type Set TrivialDomain = [Int]
    type Engine TrivialDomain = IO

    top _ = return [0..10]

    complement d a = top d >>= (flip (exclude d)) a
    exclude d a b  = return $ filter (not . (`elem` b)) a

此处,Proxy d 的目的是准确指定您要使用的实例。

但是,这意味着我们必须在每个方法的用法上写top (Proxy :: Proxy d)(与其他方法类似),这是相当繁重的。使用 GHC 8,我们可以省略 Proxys 并使用 TypeApplications 代替:

{-# language TypeApplications, TypeFamilies #-}

-- ...

instance Domain TrivialDomain where
    type Set TrivialDomain = [Int]
    type Engine TrivialDomain = IO

    top = return [0..10]

    complement a = top @TrivialDomain >>= (flip (exclude @TrivialDomain)) a
    exclude a b = return $ filter (not . (`elem` b)) a