如果不在Eq类中,Haskell整数文本如何具有可比性



在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类型的变量x1

然后执行y = 43,这类似地导致变量y的类型为forall q. Num q => q2

然后输入x == y,GHCi必须进行评估才能打印TrueFalse。如果不为pq选择具体类型(必须相同),则无法执行该评估。对于==的定义,每种类型都有自己的代码,因此如果不决定使用哪种类型的代码,就无法运行==的代码3

然而,xy中的每一个都可以被用作Num中的任何类型(因为它们有一个适用于所有类型的定义)4。因此,我们可以只使用(x :: Int) == y,编译器将确定它应该对==使用Int定义,或者x == (y :: Double)使用Double定义。我们甚至可以用不同的类型重复这样做!这些都没有使用来改变xy的类型;我们只是每次在它们支持的(许多)类型中的一个上使用它们。

如果没有默认的概念,一个空的x == y只会从编译器中产生一个Ambiguous type variable错误。语言设计者认为这将是非常常见的,尤其是数字文字(因为文字是多态的,但一旦你对它们进行任何操作,你就需要一个具体的类型)。因此,他们引入了一些规则,即如果允许继续编译,则一些不明确的类型变量应默认为具体类型5

因此,当您执行x == y时,实际发生的情况是,编译器只是选择Integer用于该特定表达式中的xy,因为您没有给它足够的信息来确定任何特定类型(而且默认规则适用于这种情况)。Integer有一个Eq实例,因此它可以使用它,即使最常见的xy类型不包括Eq约束。如果不选择某个东西,它甚至不可能尝试调用==(当然,它选择的"某个东西"必须在Eq中,否则它仍然无法工作)。

如果启用-Wtype-defaults(包含在-Wall中),编译器将在应用默认6时打印警告,这将使进程更加可见。


1forall p部分在标准Haskell中是隐式的,因为所有类型变量都会在它们出现的类型表达式的开头自动引入forall。您必须打开扩展,甚至手动编写forallExplicitForAll仅用于写入forall的能力,或者实际添加使forall对显式写入有用的功能的许多扩展中的任何一个。


2GHCi可能会再次选择p作为类型变量,而不是q。我只是用了一个不同的变量来强调它们是不同的变量。


3从技术上讲,并非每个类型都必须具有不同的==,而是每个Eq实例。其中一些实例是多态的,因此它们适用于多个类型,但这只会产生具有某种结构的类型(如Maybe a等)。基本类型,如IntIntegerDoubleCharBool,每个都有自己的实例,并且这些实例中的每个实例都有自己用于==的代码。


4在底层系统中,像forall p. Num p => p这样的类型实际上很像函数;一种将具体类型的CCD_ 64实例作为参数的方法。要获得一个具体的价值,你必须首先";应用函数";到一个类型的Num实例,只有这样你才能得到一个可以打印的实际值,与其他东西进行比较,等等。在标准Haskell中,编译器总是无形地传递这些实例参数;一些扩展允许您更直接地操作这个过程。

这就是为什么当xy是多态变量时,x == y会起作用的困惑根源。如果必须显式传递类型/实例参数,那么这里发生的事情就很明显了,因为您必须手动将xy应用于某个对象并比较结果。


5默认规则的要点是,如果对不明确类型变量的约束是:

  1. 所有内置类
  2. 其中至少有一个是数字类(NumFloating等)

则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)。

相关内容

  • 没有找到相关文章

最新更新