在Haskell中(至少在GHC v8.8.4中),处于Num
类并不意味着处于Eq
类:
$ ghci
GHCi, version 8.8.4: https://www.haskell.org/ghc/ :? for help
λ>
λ> let { myEqualP :: Num a => a -> a -> Bool ; myEqualP x y = x==y ; }
<interactive>:6:60: error:
• Could not deduce (Eq a) arising from a use of ‘==’
from the context: Num a
bound by the type signature for:
myEqualP :: forall a. Num a => a -> a -> Bool
at <interactive>:6:7-41
Possible fix:
add (Eq a) to the context of
the type signature for:
myEqualP :: forall a. Num a => a -> a -> Bool
• In the expression: x == y
In an equation for ‘myEqualP’: myEqualP x y = x == y
λ>
这似乎是因为例如,可以为某些函数类型定义Num
实例。
此外,如果我们防止ghci
过度使用整数文字的类型,它们只有Num
类型约束:
λ>
λ> :set -XNoMonomorphismRestriction
λ>
λ> x=42
λ> :type x
x :: Num p => p
λ>
因此,像上面的x或42这样的术语没有理由具有可比性。
但是,他们碰巧是:
λ>
λ> y=43
λ> x == y
False
λ>
有人能解释这个明显的悖论吗?
如果不使用Eq
,就无法比较整数文本。但事实并非如此。
在GHCi中,在NoMonomorphismRestriction
(这是目前GHCi的默认值;在GHC 8.8.4中不确定)下,x = 42
导致forall p :: Num p => p
类型的变量x
1
然后执行y = 43
,这类似地导致变量y
的类型为forall q. Num q => q
2
然后输入x == y
,GHCi必须进行评估才能打印True
或False
。如果不为p
和q
选择具体类型(必须相同),则无法执行该评估。对于==
的定义,每种类型都有自己的代码,因此如果不决定使用哪种类型的代码,就无法运行==
的代码3
然而,x
和y
中的每一个都可以被用作Num
中的任何类型(因为它们有一个适用于所有类型的定义)4。因此,我们可以只使用(x :: Int) == y
,编译器将确定它应该对==
使用Int
定义,或者x == (y :: Double)
使用Double
定义。我们甚至可以用不同的类型重复这样做!这些都没有使用来改变x
或y
的类型;我们只是每次在它们支持的(许多)类型中的一个上使用它们。
如果没有默认的概念,一个空的x == y
只会从编译器中产生一个Ambiguous type variable
错误。语言设计者认为这将是非常常见的,尤其是数字文字(因为文字是多态的,但一旦你对它们进行任何操作,你就需要一个具体的类型)。因此,他们引入了一些规则,即如果允许继续编译,则一些不明确的类型变量应默认为具体类型5
因此,当您执行x == y
时,实际发生的情况是,编译器只是选择Integer
用于该特定表达式中的x
和y
,因为您没有给它足够的信息来确定任何特定类型(而且默认规则适用于这种情况)。Integer
有一个Eq
实例,因此它可以使用它,即使最常见的x
和y
类型不包括Eq
约束。如果不选择某个东西,它甚至不可能尝试调用==
(当然,它选择的"某个东西"必须在Eq
中,否则它仍然无法工作)。
如果启用-Wtype-defaults
(包含在-Wall
中),编译器将在应用默认6时打印警告,这将使进程更加可见。
1forall p
部分在标准Haskell中是隐式的,因为所有类型变量都会在它们出现的类型表达式的开头自动引入forall
。您必须打开扩展,甚至手动编写forall
;ExplicitForAll
仅用于写入forall
的能力,或者实际添加使forall
对显式写入有用的功能的许多扩展中的任何一个。
2GHCi可能会再次选择p
作为类型变量,而不是q
。我只是用了一个不同的变量来强调它们是不同的变量。
3从技术上讲,并非每个类型都必须具有不同的==
,而是每个Eq
实例。其中一些实例是多态的,因此它们适用于多个类型,但这只会产生具有某种结构的类型(如Maybe a
等)。基本类型,如Int
、Integer
、Double
、Char
、Bool
,每个都有自己的实例,并且这些实例中的每个实例都有自己用于==
的代码。
4在底层系统中,像forall p. Num p => p
这样的类型实际上很像函数;一种将具体类型的CCD_ 64实例作为参数的方法。要获得一个具体的价值,你必须首先";应用函数";到一个类型的Num
实例,只有这样你才能得到一个可以打印的实际值,与其他东西进行比较,等等。在标准Haskell中,编译器总是无形地传递这些实例参数;一些扩展允许您更直接地操作这个过程。
这就是为什么当x
和y
是多态变量时,x == y
会起作用的困惑根源。如果必须显式传递类型/实例参数,那么这里发生的事情就很明显了,因为您必须手动将x
和y
应用于某个对象并比较结果。
5默认规则的要点是,如果对不明确类型变量的约束是:
- 所有内置类
- 其中至少有一个是数字类(
Num
、Floating
等)
则GHC将尝试Integer
,以查看该类型是否检查并允许解决所有其他约束。如果这不起作用,它将尝试Double
,如果不起作用,则报告错误。
您可以使用default
声明设置它将尝试的类型("默认默认值"为default (Integer, Double)
),但您无法自定义它尝试默认的条件,因此根据我的经验,更改默认类型的用处有限。
然而,GHCi附带了扩展的默认规则,这些规则在解释器中更有用(因为它必须逐行进行类型推断,而不是一次对整个模块进行类型推断)。您可以在具有ExtendedDefaultRules
扩展名的编译代码中打开这些选项(或者在具有NoExtendedDefaultRules
的GHCi中关闭它们),但同样,根据我的经验,这两个选项都不是特别有用。解释器和编译器的行为不同是令人讨厌的,但每次模块编译和每次行解释之间的根本区别意味着,将其中一个的默认规则切换为与另一个一致工作更令人讨厌。(这也是为什么NoMonomorphismRestriction
现在默认在解释器中有效的原因;单态性限制在编译代码中很好地实现了其目标,但在解释器会话中几乎总是错误的)。
6您也可以将类型化的孔与asTypeOf
辅助对象结合使用,以获得GHC,告诉您它推断出的子表达式的类型如下:
λ :t x
x :: Num p => p
λ :t y
y :: Num p => p
λ (x `asTypeOf` _) == y
<interactive>:19:15: error:
• Found hole: _ :: Integer
• In the second argument of ‘asTypeOf’, namely ‘_’
In the first argument of ‘(==)’, namely ‘(x `asTypeOf` _)’
In the expression: (x `asTypeOf` _) == y
• Relevant bindings include
it :: Bool (bound at <interactive>:19:1)
Valid hole fits include
x :: forall p. Num p => p
with x
(defined at <interactive>:1:1)
it :: forall p. Num p => p
with it
(defined at <interactive>:10:1)
y :: forall p. Num p => p
with y
(defined at <interactive>:12:1)
你可以看到它告诉我们漂亮而简单的Found hole: _ :: Integer
,然后继续它喜欢给我们的关于错误的所有额外信息。
类型化的孔(最简单的形式)只意味着用_
代替表达式。编译器在这样一个表达式上出错,但它试图给你提供关于你可以使用什么来"编译"的信息;"填空";以便进行编译;最有用的是,它告诉你在那个位置上有效的东西的类型。
CCD_ 83是用于添加一位类型信息的旧模式。它返回foo
,但它将其限制为与bar
相同的类型(bar
的实际值完全未使用)。因此,如果您已经有一个类型为Double
的变量d
,那么x `asTypeOf` d
将是x
的值,作为Double
。
这里我使用CCD_ 92";向后";;我不是用右边的东西来约束左边的东西的类型,而是在右边放一个洞(可以有任何类型),但asTypeOf
可以方便地确保它与x
是同一类型,而不会改变x
在整个表达式中的使用方式(因此,相同的类型推断仍然适用,包括默认情况,如果您将较大表达式的一小部分取出,用:t
询问GHCi的类型,则情况并非总是如此;特别是:t x
不会告诉我们Integer
,而是Num p => p
)。