【问题标题】:Fundamentals of Haskell Type Classes and "could not deduce (~) from the context (~)" errorHaskell 类型类的基本原理和“无法从上下文 (~) 推断 (~)”错误
【发布时间】:2014-01-06 23:41:08
【问题描述】:

我对 Haskell 比较陌生,我相信我误解了关于类型类的一些基本知识。假设我想创建一个类型类“T”,实现由四种代数类型“A、B、C 和 D”支持的 n 元树,其结构规定最大深度为 4。这似乎是一个愚蠢的例子,但我认为它最能说明我的观点。

module Test where

class T t0 where
    parent :: T t1 => t0 -> Maybe t1
    children :: T t1 => t0 -> [t1]

data A = A [B]
instance T A where
    parent (A _) = Nothing
    children (A bs) = bs

data B = B A [C]
instance T B where
    parent (B a _) = Just a
    children (B _ cs) = cs

data C = C B [D]
instance T C where
    parent (C b _) = Just b
    children (C _ ds) = ds

data D = D C
instance T D where
    parent (D c) = Just c
    children (D _) = []

我想编写通用的父子函数,但 GHC 没有。

Test.hs:10:27:
    Could not deduce (t1 ~ B)
    from the context (T t1)
      bound by the type signature for children :: T t1 => A -> [t1]
      at Test.hs:10:9-28
      `t1' is a rigid type variable bound by
           the type signature for children :: T t1 => A -> [t1]
           at Test.hs:10:9
    Expected type: [t1]
      Actual type: [B]
    In the expression: bs
    In an equation for `children': children (A bs) = bs
    In the instance declaration for `T A'

Test.hs:14:31:
    Could not deduce (t1 ~ A)
    from the context (T t1)
      bound by the type signature for parent :: T t1 => B -> Maybe t1
      at Test.hs:14:9-31
      `t1' is a rigid type variable bound by
           the type signature for parent :: T t1 => B -> Maybe t1
           at Test.hs:14:9
    In the first argument of `Just', namely `a'
    In the expression: Just a
    In an equation for `parent': parent (B a _) = Just a

Test.hs:15:29:
    Could not deduce (t1 ~ C)
    from the context (T t1)
      bound by the type signature for children :: T t1 => B -> [t1]
      at Test.hs:15:9-30
      `t1' is a rigid type variable bound by
           the type signature for children :: T t1 => B -> [t1]
           at Test.hs:15:9
    Expected type: [t1]
      Actual type: [C]
    In the expression: cs
    In an equation for `children': children (B _ cs) = cs
    In the instance declaration for `T B'

Test.hs:19:31:
    Could not deduce (t1 ~ B)
    from the context (T t1)
      bound by the type signature for parent :: T t1 => C -> Maybe t1
      at Test.hs:19:9-31
      `t1' is a rigid type variable bound by
           the type signature for parent :: T t1 => C -> Maybe t1
           at Test.hs:19:9
    In the first argument of `Just', namely `b'
    In the expression: Just b
    In an equation for `parent': parent (C b _) = Just bv

我(我想我)理解类型类根本不像 Java 接口,因为类级函数必须适用于所提供类型变量的任何可能值;调用者没有“决定”类型。我不明白为什么 GHC 不能推导出 (t1 ~ _),因为替换 t1 的类型始终是 'T' 的实例。我看到实例声明之间存在一种循环依赖关系,例如A 的实例声明取决于 B 的有效性,这取决于 A 和 C 的,依此类推,但我觉得 GHC 足够聪明,可以弄清楚这一点,而我只是错过了一些东西。每当我希望类型类中的函数接受类中的一种类型但返回另一种类型时,我似乎总是会收到此错误...有没有办法使用类型类来完成此操作?

我看到这里有许多措辞相似的问题,但我还没有找到一个与我的问题相匹配的问题(据我所知)。

提前致谢。

【问题讨论】:

    标签: haskell functional-programming typeclass


    【解决方案1】:

    你已经正确理解了这个问题:这些签名真的意味着

    parent :: forall t1 . T t1 => t0 -> Maybe t1
    

    而不是你在协变 OO 语言中所拥有的,

    parent :: exists t1 . T t1 => t0 -> Maybe t1
    

    这类东西的两种常用解决方案是使用相似的句法扩展

    • TypeFamilies

      class T t0 where
        type Child t0 :: *
        parent :: Child t0 -> Maybe t0
        children :: t0 -> [Child t 0]
      
      instance T A where
        type Child A = B
        parent (A _) = Nothing
      
      ...
      
    • MultiparamTypeClasses

      class T child t0 where
        parent :: child -> Maybe t0
        children :: t0 -> [child]
      
      instance T A B where
      ...
      

    请注意,在这两种情况下,D 都没有实例。

    至于这些扩展中哪个“更好”——你无法真正回答这个问题。 MultiparamTypeClasses 本身通常太“弱”,因为您需要手动修复所有涉及的类型,但这可以通过添加 FunctionalDependency 来解除。在class T child t0 | t0 -> child 的特殊情况下,这在很大程度上等同于TypeFamilies 解决方案。但在你的情况下,class T child t0 | t0 -> child, child -> t0 也是可能的。

    有关详细信息,请考虑Haskell Wiki

    【讨论】:

    • 正是我要说的。
    • 好的,所以我了解类型族在这种情况下以及任意更复杂的情况下会如何帮助我。如果我推断类型族在类型级别而不是数据级别启用某种偏函数,我是否正确?
    • “类型级函数”正是这些 type family / 关联的 type 声明通常被称为。
    【解决方案2】:

    这确实不是您特定问题的答案,但它是您问题的解决方案:创建一个最大深度受其类型限制的 k-ary 树结构。如果您不关心使用大量 GHC 扩展,此解决方案非常简单且可扩展。

    我不会详细介绍 - 某些扩展的工作原理非常复杂,如果您想了解详细信息,您应该阅读文档。

    {-# LANGUAGE 
        MultiParamTypeClasses
      , DataKinds
      , GADTs
      , FunctionalDependencies
      , KindSignatures
      , FlexibleInstances
      , UndecidableInstances
      , PolyKinds
      , TypeOperators
      , FlexibleContexts
      , TypeFamilies
      #-} 
      -- Not all of these are needed; most are used to make the code cleaner
    
    data Nat = Z | P Nat
    

    Nat 类型用于对类型级别的深度进行编码。使用-XDataKinds,GHC 可以采用像Nat 这样的简单数据类型并“提升”它;也就是说,数据构造函数变成了类型,而Nat 类型变成了“种类”(类型的类型)。 Z == 零; P == 加一。

    type family LTEQ (a :: Nat) (b :: Nat) :: Bool
    type instance LTEQ Z Z = True
    type instance LTEQ Z (P x) = True
    type instance LTEQ (P x) Z = False
    type instance LTEQ (P a) (P b) = LTEQ a b
    

    接下来我们在Nat 上定义一个偏序。注意明确的种类签名(即-a :: Nat)-PolyKinds 不需要这些签名,但可以更清楚地了解正在发生的事情。如果这看起来令人困惑,只需将其视为一组规则:

    0 <= 0 == True
    0 <= (1 + x) == True
    (1 + x) <= 0 == False
    (1 + x) <= (1 + y) == x <= y
    

    如果你想向自己证明这是可行的:

    -- This would only be used for testing
    data Pr p = Pr
    
    lteq :: f ~ (a `LTEQ` b) => Pr a -> Pr b -> Pr f
    lteq _ _ = Pr
    
    >:t lteq (Pr :: Pr (P Z)) (Pr :: Pr Z)
    lteq (Pr :: Pr (P Z)) (Pr :: Pr Z) :: Pr Bool 'False
    >:t lteq (Pr :: Pr (P Z)) (Pr :: Pr (P Z))
    lteq (Pr :: Pr (P Z)) (Pr :: Pr (P Z)) :: Pr Bool 'True
    >:t lteq (Pr :: Pr (P Z)) (Pr :: Pr (P (P Z)))
    lteq (Pr :: Pr (P Z)) (Pr :: Pr (P (P Z))) :: Pr Bool 'True
    >:t lteq (Pr :: Pr Z) (Pr :: Pr (P (P Z)))
    lteq (Pr :: Pr Z) (Pr :: Pr (P (P Z))) :: Pr Bool 'True
    >:t lteq (Pr :: Pr Z) (Pr :: Pr Z)
    lteq (Pr :: Pr Z) (Pr :: Pr Z) :: Pr Bool 'True
    

    我们必须使用Pr 而不仅仅是a -&gt; b -&gt; (LTEQ a b),因为ab 是提升类型(特别是Nat 类型),而(-&gt;) 只接受未提升类型。如果这没有意义,请不要太担心,因为这并不重要。说这里需要它就够了。

    定义最大深度非常简单:

    type MAXDEPTH = P (P (P (P Z)))
    

    请注意更改最大深度是多么简单。现在是 Tree 数据类型。它使用 GADT(广义代数数据类型)语法;基本上所有这些意味着我们可以更好地控制你如何创建Tree类型的东西。 d 类型变量是深度,a 是存储在树中的元素。

    data Tree d a where
        D0    :: a -> Tree Z a     
        DPlus :: ((P d) `LTEQ` MAXDEPTH) ~ True => a -> [Tree d a] -> Tree (P d) a 
    

    让我们通过构造函数来分解它。第一个,D0 接受 a 类型的值并创建一个深度为零的树,只存储该值:只有一个没有子节点的节点。

    DPlus 接受一个节点和一个子树列表。添加一个节点显然会使深度增加一倍 - 您可以看到结果类型反映了这一点。然后,为了强制执行最大深度 4,我们只说d + 1 &lt;= MAXDEPTH

    因为深度为 0 的树很无聊,你可能需要一个深度为 1 的辅助函数:

    depth1 a xs = DPlus a (map D0 xs)
    

    然后一个节目实例只是为了好玩:

    instance Show a => Show (Tree d a) where
        show (D0 a)     = "D0 " ++ show a
        show (DPlus a xs) = "DPlus " ++ show a ++ " " ++ show xs
    

    还有一个快速测试:

    >depth1 'c' "hello"
    DPlus 'c' [D0 'h',D0 'e',D0 'l',D0 'l',D0 'o']
    
    >DPlus 'a' [depth1 'c' "hello"]
    DPlus 'a' [DPlus 'c' [D0 'h',D0 'e',D0 'l',D0 'l',D0 'o']]
    
    >DPlus 'b' [DPlus 'a' [depth1 'c' "hello"]]
    DPlus 'b' [DPlus 'a' [DPlus 'c' [D0 'h',D0 'e',D0 'l',D0 'l',D0 'o']]]
    
    >DPlus 'c' [DPlus 'b' [DPlus 'a' [depth1 'c' "hello"]]]
    DPlus 'c' [DPlus 'b' [DPlus 'a' [DPlus 'c' [D0 'h',D0 'e',D0 'l',D0 'l',D0 'o']]]]
    
    >DPlus 'd' [DPlus 'c' [DPlus 'b' [DPlus 'a' [depth1 'c' "hello"]]]]
    <interactive>:130:1:
        Couldn't match type 'False with 'True
        Expected type: 'True ...
    

    如您所见,尝试构建深度大于 4 的树会导致类型错误。

    快速说明:您的示例代码适用于允许您参考“备份”其结构的树。由于我回答的主要目的是演示使用 DataKinds 在类型级别上强制执行树深度,因此我只是实现了一个非常简单的树。您有正确的想法来引用“向上”树,但由于现在所有内容都是单一类型,您可能甚至不需要类型类!

    【讨论】:

    • 为什么不导入GHC.TypeLitsDataKinds 有很多应用程序,但几乎所有这样的 StackOverflow 答案都是从定义 data Nat 开始的。有点奇怪的海事组织。
    • GHC.TypeLits.Nat 还没有工作,加法类型族还没有实现,所以没有办法定义LTEQ (这也将在GHC.TypeLits 中实现) 并且无法定义 DPlus 构造函数。我相信,在即将发布的版本中一切都应该正常运行。
    猜你喜欢
    • 2015-08-28
    • 1970-01-01
    • 2011-01-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-05-10
    • 1970-01-01
    相关资源
    最近更新 更多