正如预期的那样,这工作正常:
foo :: Fractional a => a
foo = undefined -- datum
bar :: Num a => a -> a
bar a = undefined -- function
baz :: Fractional a => a
baz = bar foo -- application
这按预期工作,因为每个Fractional
也是一个Num
。
因此,正如预期的那样,我们可以将Fractional
参数传递到Num
参数中。
另一方面,以下内容也有效。我不明白为什么。
foo :: Fractional a => a -> a
foo a = undefined -- function
bar :: Num a => a
bar = undefined -- datum
baz :: Fractional a => a
baz = foo bar -- application
出乎意料地工作!有些Num
不是Fractionals
.
那么,为什么我可以将Num
参数传递到Fractional
参数中呢?你能解释一下吗?
Chi的回答对正在发生的事情给出了很好的高层次解释。我认为给出一种稍微低级(但也更机械)的方式来理解这一点也可能很有趣,这样你就可以处理其他类似的问题,转动曲柄,得到正确的答案。我将讨论类型作为该类型值的用户和实现者之间的一种协议。
- 对于
forall a. t
,调用方可以选择一种类型,然后他们继续协议t
(其中a
已被替换为t
中任何地方的调用方选择)。 - 对于
Foo a => t
,调用方必须向实现者提供证明a
是Foo
的实例。然后他们继续协议t
. - 对于
t1 -> t2
,调用者可以选择类型t1
的值(例如,通过运行协议t1
,实现者和调用者的角色切换)。然后他们继续协议t2
. - 对于任何类型的
t
(即,在任何时候),实现者可以通过生成适当类型的值来缩短协议。如果上述规则都不适用(例如,如果我们达到了像Int
这样的基本类型或像a
这样的裸类型变量),实现者必须这样做。
现在,让我们为您的术语提供一些不同的名称,以便我们区分它们:
valFrac :: forall a. Fractional a => a
valNum :: forall a. Num a => a
idFrac :: forall a. Fractional a => a -> a
idNum :: forall a. Num a => a -> a
我们还想探索两个定义:
applyIdNum :: forall a. Fractional a => a
applyIdNum = idNum valFrac
applyIdFrac :: forall a. Fractional a => a
applyIdFrac = idFrac valNum
让我们先谈谈applyIdNum
。协议说:
- 呼叫者选择类型
a
。 - 来电者证明它是
Fractional
。 - 实现者提供类型为
a
的值。
该实现说:
实施者以调用方身份启动
idNum
协议。因此,她必须:- 选择类型
a
.她悄悄地做出了和来电者一样的选择。 - 证明
a
是Num
的实例。这没问题,因为她其实知道a
是Fractional
,这意味着Num
。 - 提供类型为
a
的值。在这里,她选择了valFrac
.为了完整,她必须证明valFrac
具有类型a
.
- 选择类型
因此,实现者现在运行
valFrac
协议。她:- 选择类型
a
。在这里,她悄悄地选择了idNum
期待的类型,这恰好与她的呼叫者为a
选择的类型相同。 - 证明
a
是Fractional
的实例。她使用与呼叫者相同的证明。 - 然后
valFrac
的实现者承诺根据需要提供类型为a
的值。
- 选择类型
为了完整起见,这里是applyIdFrac
的类似讨论。协议说:
- 呼叫者选择
a
的类型。 - 来电者证明
a
是Fractional
。 - 实现者必须提供类型为
a
的值。
该实现说:
实施者将执行
idFrac
协议。因此,她必须:- 选择一种类型。在这里,她悄悄地选择她的呼叫者选择的任何东西。
- 证明
a
是Fractional
.她传递了来电者的证明。 - 选择类型为
a
的值。她将执行valNum
协议来执行此操作;我们必须检查这是否产生一个a
类型的值 .
在执行
valNum
协议期间,她:- 选择一种类型。在这里,她选择了
idFrac
期望的类型,即a
;这也恰好是她的呼叫者选择的类型。 - 证明
Num a
成立。她可以这样做,因为她的调用者提供了Fractional a
的证明,您可以从Fractional a
的证明中提取Num a
的证明。 - 然后,
valNum
的实现者根据需要提供类型为a
的值。
- 选择一种类型。在这里,她选择了
有了现场的所有细节,我们现在可以尝试缩小并查看大局。applyIdNum
和applyIdFrac
具有相同的类型,即forall a. Fractional a => a
.因此,在这两种情况下,实现者都可以假设a
是Fractional
的实例。但是,由于所有Fractional
实例都是Num
实例,这意味着实现者可以假设Fractional
和Num
都适用。这使得在实现中使用假定任一约束的函数或值变得容易。
附言我反复使用副词"悄悄地"来选择forall a. t
协议期间所需的类型。这是因为Haskell非常努力地向用户隐藏这些选择。但是,如果您喜欢TypeApplications
扩展名,则可以明确表示它们;在协议中选择类型t
f
将使用语法f @t
。不过,实例证明仍代表您静默管理。
baz :: Fractional a => a
中的a
类型由调用baz
的人选择。他们有责任保证他们选择的a
类型属于Fractional
类。因为Fractional
是Num
的子类,因此a
类型也必须是Num
。因此,baz
可以同时使用foo
和bar
。
换句话说,由于子类关系,签名
baz :: Fractional a => a
本质上等同于
baz :: (Fractional a, Num a) => a
你的第二个例子实际上和第一个是同一种,foo, bar
之间的哪一个是函数,哪一个是参数并不重要。您也可以考虑这一点:
foo :: Fractional a => a
foo = undefined
bar :: Num a => a
bar = undefined
baz :: Fractional a => a
baz = foo + bar -- Works
预期工作,因为每个
Fractional
也是一个Num
。
这是正确的,但重要的是要准确理解这意味着什么。这意味着:Fractional
类中的每个类型也属于Num
类。这并不意味着具有OO或动态背景的人可能会理解:"Num
类型中的每个值也属于Fractional
类型"。如果是这种情况,那么您的推理将是有意义的:那么Num
值bar
将不够通用,无法在foo
函数中使用。
。或者实际上不会,因为在 OO 语言中,数字层次结构将在另一个方向上工作——其他语言通常允许您将任何数值转换为小数,但另一个方向在这些语言中会产生 round,这合理的强类型不会自动做到!
在 Haskell 中,您不需要担心这些,因为永远不会有任何隐式类型转换。bar
和foo
处理完全相同的类型,此类型发生在变量a
是次要的。现在,bar
和foo
都以不同的方式约束此单一类型,但由于它是受约束的同一类型,因此您只需获得两种约束的组合(Num a, Fractional a)
,由于Num a => Fractional a
,这相当于单独Fractional a
。
TL;DR:并不是说Num a => a
是一个Num
值,而是一个定义,可以是任何类型的Num
的值,无论该类型是什么,具体来说,由使用它的每个特定位置决定。
我们先定义它,然后再使用它。
如果我们笼统地定义了它,以便它可以在许多不同的特定类型中使用,那么我们以后可以在许多不同的使用站点使用它,每个站点都需要根据我们的定义为其提供特定类型的值。只要该特定类型符合定义和使用站点的类型约束。
这就是多态定义
的意义所在。它不是一个多态值。这是来自动态世界的概念,但我们的概念是静态的。Haskell中的类型不是在运行时决定的。它们是预先知道的。
这是发生的情况:
> numfunc :: Num a => a -> a; numfunc = undefined
> fraval :: Fractional a => a; fraval = undefined
> :t numfunc fraval
numfunc fraval :: Fractional a => a
numfunc
要求其论点Num
.fraval
是一个多态定义,能够提供特定用途可能要求的任何类型的Fractional
数据。不管它是什么,因为它在Fractional
,它保证也在Num
,所以它是可以接受的numfunc
.
由于我们现在知道a
在Fractional
中(由于fraval
),因此现在已知整个应用程序的类型也处于Fractional
中(因为numfunc
的类型)。
技术
fraval :: Fractional a => a -- can provide any Fractional
numfunc :: Num a => a -> a -- is able to process a Num
-------------------------------------------------
numfunc fraval :: (Num a, Fractional a) => a -- can provide a Fractional
并且(Num a, Fractional a)
简化为类型类的交集,即只是Fractional a
.
这当然意味着,如果代码的其余部分没有其他内容,进一步指定类型,我们将得到一个模棱两可的类型错误(除非某些类型默认启动)。但可能有。目前这是可以接受的,并且有一个类型 - 多态类型,这意味着,其他东西必须在代码的其余部分,在任何特定的使用站点上进一步指定它。所以就目前而言,作为一个一般的多态定义,这是完全可以接受的。
<小时 />下一页,
> frafunc :: Fractional a => a -> a; frafunc = undefined
> numval :: Num a => a; numval = undefined
> :t frafunc numval
frafunc numval :: Fractional a => a
frafunc
要求其类型在Fractional
.numval
能够提供所需的任何类型的数据,只要该类型处于Num
。因此,它非常乐意满足对Fractional
价值的任何需求。当然,代码中的其他内容将不得不进一步专门化类型,但无论如何。现在一切都很好。
技术
numval :: Num a => a -- can provide any Num
frafunc :: Fractional a => a -> a -- is able to process a Fractional
-------------------------------------------------
frafunc numval :: (Num a, Fractional a) => a -- can provide any Fractional
(我发布这个答案是因为我认为最简单的事情可能是初学者的绊脚石,而这些最简单的事情可以被专家在不知不觉中视为理所当然。俗话说,我们不知道是谁发现了水,但肯定不是鱼。