【问题标题】:Type ambiguity in Haskell type familiesHaskell 类型族中的类型歧义
【发布时间】:2016-03-23 10:38:51
【问题描述】:

我正在尝试将以下类 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 不这么认为。我错过了什么?

【问题讨论】:

    标签: haskell type-inference type-families


    【解决方案1】:

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

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

    由于您的类方法仅提及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
    

    最后,如果你想使用你的原始类,你必须在你的方法中添加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
    

    【讨论】:

    • 关于最后一个例子,你能在 GHC8 的类级别写top @d 吗?
    • 我认为我们不能。默认情况下,forall-bound 类型变量可以是@-applied,但显然我们不能在Domain 方法类型中写forall d.。我发现@在类方法上效果很好,可以按照类类型参数的顺序使用。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-01-10
    • 2023-04-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多