【问题标题】:I'm having trouble with a multi-parameter typeclass我在使用多参数类型类时遇到问题
【发布时间】:2019-09-29 18:41:22
【问题描述】:

调用 'Foo' 的 'bar' 方法,我得到一个错误,它无法统一 3 和 4 的类型,因为它们是重载的文字。但是看起来相同类型的“standaloneBar”工作正常。区别一定是类型类参数,但我不明白为什么这会阻止统一。

{-# LANGUAGE MultiParamTypeClasses #-}
module Main where

class Foo a b where
  bar :: a -> b -> a

data Baz a = Baz a
instance Foo Int (Baz a) where
  bar i (Baz _) = i

standaloneBar :: a -> b -> a
standaloneBar x _ = x

main = do
  --putStrLn $ show $ bar 3 (Baz 4)          -- Can't unify
  putStrLn $ show $ standaloneBar 3 (Baz 4)  -- Works fine
  putStrLn $ show $ bar (3::Int) (Baz 4)     -- Works fine
  putStrLn $ show $ ((bar 3 (Baz 4)) :: Int) -- Works fine

如果我添加类型注释,那么它工作正常。

我这里理解统一的方式,虽然3和4有歧义,但还是可以统一的:

*Util Delta Exp Tmi Util> :t 3
3 :: Num p => p
*Util Delta Exp Tmi Util> :t 4
4 :: Num p => p
*Util Delta Exp Tmi Util> :t 3 + 4
3 + 4 :: Num a => a

那么为什么它不能对'bar'做同样的事情呢?

(我意识到这里的功能依赖可以解决问题,但我特别尝试允许多个实例,而这会被阻止。)

【问题讨论】:

    标签: haskell typeclass


    【解决方案1】:

    编译器必须考虑到以后,可能在另一个模块中,有人定义了类似的东西

    instance Foo Double (Baz a) where
      bar i (Baz _) = i + 1
    

    在这种情况下,putStrLn $ show $ bar 3 (Baz 4) 可以打印 34.0,具体取决于文字 3 的类型。因此,它被拒绝了。

    请注意,错误中提到了歧义,而不是统一失败:

    prog.hs:16:14: error:
        • Ambiguous type variable ‘a0’ arising from a use of ‘show’
          prevents the constraint ‘(Show a0)’ from being solved.
          Probable fix: use a type annotation to specify what ‘a0’ should be.
    

    在您的 GHCi 会话中,> :t 3 + 4 可以输出 Num a => a,因为它可以报告多态类型。如果你运行> :t show (3+4),结果是一个单态String,这迫使GHCi选择一个特定类型a来实例化常量。 Num 碰巧受到 Haskell 的特别关注,并且在发生这种情况时会尝试一些“默认”类型。这确实被称为“默认”,并且只发生在少数Prelude 类中。它不适用于自定义类,例如您的 Foo,而是会报告歧义。

    【讨论】:

    • 感谢您非常清楚的解释!我想知道您是否可以建议另一种不需要大量显式注释的方法。我正在尝试做的是允许为任何给定类型创建一组开放式增量类型,这些增量类型表示对该类型值的更改。这就是为什么我想放弃函数依赖。我怀疑类型族可能有答案,但我首先了解了这些。
    • @gregorymichaeltravis 你真的需要相同的增量类型来应用于许多类型。我明白你为什么不想要fundep a -> b,但也许可以添加b -> a 而不会损失太多?
    • 不,你完全正确,我没想到。 delta 只需要适用于一种类型。
    【解决方案2】:

    standaloneBar 并不真正具有相同的类型。它是a -> b -> a,与 中为bar 指定的类型相同。但问题不在于bar 3 (Baz 4) 未能匹配类中的通用类型,而是bar 3 (Baz 4) 未能唯一确定一个实例

    当涉及到类型类时,类型推断不仅要确定存在一些类型良好的类型变量赋值,它还必须实际决定选择哪个特定实例1。不同的实例可能有非常不同的行为,所以选择很重要。

    编译器推断在您的代码中使用bar 的类型是bar :: (Foo a (Baz b), Num a, Num b, Show a) => a -> Baz b -> aShow 约束来自传递给show 的结果)。现在,来自Foo Int (Baz a) 实例的bar 的版本具有Int -> Baz a -> Int 类型,这显然确实 与您对bar 的用法一致。但其他可能的情况也可能统一。可能有Foo Double (Baz a),或Foo a (Baz Float),或任何数量的其他可能性。

    编译器可以通过选择Foo Int (Baz a) 来工作,因为这不是范围内的唯一实例,而且它确实适合。然而,语言规则的设计使得编译器在确定需要哪个实例时实际上不应该考虑范围内的实例!需要从调用的上下文中唯一清楚地清除适当的实例,然后编译器会检查这样的实例是否实际可用。所以需要有一个多态的Foo a (Baz b) 实例在此代码工作的范围内。事实上,如果我将您的实例替换为:

    instance Foo a (Baz b) where
      bar i _ = i
    

    然后你的代码编译并运行没有错误!

    因此,您的原始代码对类型不够具体,无法唯一确定 Foo Int (Baz b) 是必需的实例,因此编译器会报告有关不明确类型的错误。问题不在于您对bar 的使用与a -> b -> a 不统一,甚至与Int -> Baz b -> Int 不统一;它确实与两者统一。相反,问题在于它不是特定类型Int -> Baz b -> Int。添加额外的类型信息可以解决这个问题,并且是必需的。

    做出这种语言设计决定的原因是,实例在范围内的变体(例如通过添加和删除导入)永远不会将有效代码的含义更改为其他有效代码。如果删除所需的实例,代码将停止工作,如果添加冲突的实例,代码将导致错误,但如果您的代码使用其中 1 个在范围内编译 2 个实例,则永远无法继续仅通过更改您的进口来工作和使用另一个。目的是实例的选择应该是程序员提供的代码固有的要求,而不应该只是编译器意外做出的选择。


    值得注意的是,此实例选择规则有一个主要例外,它不是基于哪些实例实际上在范围内2。那就是类型默认。

    在我上面描述的规则下,涉及数字文字的简单表达式几乎总是模棱两可的。一个例子是show $ 1 + 2+show 对不同的实例有不同的行为(例如,+ 甚至不完全关联浮点数!),所以根据上面的推理,这段代码应该是无效的,程序员应该要求写show $ 1 + (2 :: Int)之类的东西。

    语言设计者认为这过于繁琐,因此定义了默认规则。它们在the Haskell Report 中有更详细的描述,但基本上,如果存在涉及“数字”类型类之一的约束(如NumIntegral 等) Prelude 和二义性类型没有任何类型类约束除了那些涉及 Prelude 中定义的类。如果满足这些(非常保守的)约束,那么将尝试一些默认类型(默认类型的默认列表是Integer, Double,但可以自定义;但是无法自定义尝试默认的条件),如果其中之一允许为所有约束找到实例,那么编译器将接受代码并为您选择这些实例。

    这意味着bar :: (Foo a (Baz b), Num a, Num b, Show a) => a -> Baz b -> a中的二义性类型变量ab不能被默认;他们参与了约束Foo a (Baz b),这不适用于 Prelude 中定义的类。

    但是当你使用standaloneBar(或者你使用我上面的更多态的Foo a (Baz b)实例)时,编译器无法解决的唯一约束是(Num a, Num b, Show a)。这里所有的约束都是 Prelude 类,而Num 是一个“数字类”,所以编译器会为ab 尝试Integer,这样可以工作并允许代码编译。


    1除非类型类约束可以在调用者的签名中“传递”,但是这里的调用者是main :: IO (),所以这个选项不可用。

    2如果你启用更多的扩展,比如OverlappingInstances,那么会有更多的例外,但我在这里不做描述。

    【讨论】:

    • 也感谢您提供这个非常明确的答案。除了更好地理解它之外,您的第一个脚注让我意识到在大多数情况下需要注释可能不会成为负担。
    • 我很好奇的一件事——您的完全多态版本与调用站点相结合,但假设的Foo Double (Baz a)Foo a (Baz Float) 也是如此——为什么这不会引起歧义?是因为完全多态的包含/包含了两个更具体的吗?
    • @gregory 是的,因为当多态实例存在时重叠实例(没有扩展)是无效的,那么就不能存在更具体的实例。所以当编译器试图解决多态约束Foo a (Bar b)时,它只需要寻找一个多态实例;如果它找到一个通用的,那么就不存在特定的,因此没有歧义,如果它没有找到一个通用的,那么它可能会抛出一个错误,因为它会是模糊的。
    猜你喜欢
    • 2018-01-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-06-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多