我在使用多参数类型类时遇到麻烦

时间:2019-09-29 18:41:22

标签: haskell typeclass

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

{-# 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'做同样的事情?

(我意识到这里的功能依赖性可以解决该问题,但是我正在特别尝试允许使用多个实例来避免这种情况。)

2 个答案:

答案 0 :(得分:3)

编译器必须考虑到以后可能在另一个模块中有人定义类似内容的可能性

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

在这种情况下,putStrLn $ show $ bar 3 (Baz 4)可以根据文字3的类型打印4.03。因此,它被拒绝了。

请注意,该错误提及歧义,而不是统一失败:

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,在其中报告了歧义。

答案 1 :(得分:0)

standaloneBar实际上不是同一类型。它是a -> b -> a,与 class 中为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的结果)。现在,来自bar实例的Foo Int (Baz a)的版本具有类型Int -> Baz a -> Int,显然,确实与您使用bar统一。但是其他可能的情况也可能统一。可能有Foo Double (Baz a)Foo a (Baz Float),或任何其他可能性。

编译器可以通过选择Foo Int (Baz a)来工作,因为这不是作用域中的唯一实例,并且确实合适。但是,设计语言规则是为了使编译器在确定需要哪个实例时实际上不应考虑范围内的实例!要求从调用上下文中唯一清除适当的实例,然后 then 编译器检查该实例是否实际可用。因此,必须有一个多态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中有更详细的描述,但是基本上,如果存在涉及“数字”类型类之一(例如Num,{{1} }等),并且歧义类型没有任何类型类约束,除了那些涉及前奏中定义的类的约束。如果满足这些(非常保守的)约束,则将尝试一些默认类型(默认类型的默认列表为Integral,但可以自定义;但是,不能自定义尝试默认的条件。 ),并且如果其中之一允许找到所有约束的实例,则编译器会为您选择这些实例,以接受代码。

这意味着Integer, Double中的模糊类型变量ab不能被默认设置;它们涉及约束bar :: (Foo a (Baz b), Num a, Num b, Show a) => a -> Baz b -> a,该约束不是针对Prelude中定义的类的。

但是,当您使用Foo a (Baz b)(或者您使用上面更具多态性的standaloneBar实例)时,编译器无法解析的唯一约束是Foo a (Baz b)。在这里,所有约束都是Prelude类,而(Num a, Num b, Show a)是一个“数字类”,因此编译器会同时为NumInteger尝试a,这可以并允许要编译的代码。


1 除非可以在调用方的签名中“传递”类型类约束,但是此处的调用方为b,因此该选项不可用。

2 如果启用了更多扩展名,例如main :: IO (),那么会有更多例外,但是我在这里不作介绍。