为什么 Num 可以像 Fractional 一样?
Why can a Num act like a Fractional?
正如预期的那样,这工作正常:
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
参数?你能解释一下吗?
baz :: Fractional a => a
中的类型a
由调用baz
的人选择。他们有责任保证他们选择的 a
类型在 Fractional
class 中。由于 Fractional
是 Num
的子 class,因此类型 a
也必须是 Num
。因此,baz
可以同时使用 foo
和 bar
。
也就是说,因为subclass关系,签名
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
chi 的回答对正在发生的事情给出了很好的高级解释。我认为提供一种稍微低级(但也更机械)的方式来理解这一点可能也很有趣,这样您就可以解决其他类似的问题,转动曲柄并获得正确的答案。我将讨论类型作为该类型值的用户与实现者之间的一种协议。
- 对于
forall a. t
,调用者可以选择一种类型,然后他们继续使用协议 t
(其中 a
在 t
).
- 对于
Foo a => t
,调用者必须向实现者提供证据证明a
是Foo
的一个实例。然后他们继续协议 t
.
- 对于
t1 -> t2
,调用者可以选择类型 t1
的值(例如,通过 运行 协议 t1
,实现者和调用者的角色互换)。然后他们继续协议 t2
.
- 对于任何类型
t
(即在任何时间),实施者可以通过生成适当类型的值来缩短协议。如果上面的规则none适用(例如,如果我们已经达到像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
都适用。这使得使用在实现中假设任一约束的函数或值变得容易。
P.S。我反复使用副词 "quietly" 来选择 forall a. t
协议期间所需的类型。这是因为 Haskell 非常努力地向用户隐藏这些选择。但是如果你喜欢使用 TypeApplications
扩展名,你可以将它们显式化;在协议 f
中选择类型 t
使用语法 f @t
。不过,实例证明仍然以您的名义默默管理。
Works as expected because every Fractional
is also a Num
.
这是正确的,但重要的是要准确说明这意味着什么。这意味着:Fractional
class 中的每个 type 也在 Num
class 中。它 不是 具有 OO 或动态背景的人可能理解的意思:“Num
类型中的每个 value 也在Fractional
类型”。如果是这种情况,那么您的推理就有意义了:那么 Num
值 bar
将不够通用,无法在 foo
函数中使用。
...或者实际上不会,因为在 OO 语言中,数字层次结构会朝另一个方向工作——其他语言通常允许您将任何数值转换为小数,但另一个方向在这些语言中会产生轮回,而强类型语言不会自动产生轮回!
在Haskell中,你需要担心none,因为从来没有任何隐式类型转换。 bar
和 foo
在完全相同的类型上工作,这种类型发生在变量 a
是次要的。现在,bar
和 foo
都以不同的方式约束这个单一类型,但是因为它是受约束的相同类型,所以您只需得到两个约束的组合 (Num a, Fractional a)
,这是由于 Num a => Fractional a
相当于单独的 Fractional a
。
TL;DR: Num a => a
不是 一个 Num
值,而是相反,它是一个定义 can 是 any 类型 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
.
这当然意味着,如果代码的其余部分没有任何其他内容,进一步指定类型,我们将得到一个不明确的类型错误(除非出现某些类型默认设置)。但是 可能 存在。现在这是可以接受的,并且有一个类型——一个 polymorphic 类型,意思是,在代码的其余部分的某个地方,在任何特定的使用地点,其他东西必须进一步指定它它出现。所以现在,作为一个通用的多态定义,这是完全可以接受的。
接下来,
> 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
(我post这个回答是因为我觉得最简单的东西对于初学者来说可能是绊脚石,而这些最简单的东西对于专家来说是理所当然的,甚至没有注意到。俗话说,我们不知道是谁发现了水,但肯定不是鱼。)
正如预期的那样,这工作正常:
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
参数?你能解释一下吗?
baz :: Fractional a => a
中的类型a
由调用baz
的人选择。他们有责任保证他们选择的 a
类型在 Fractional
class 中。由于 Fractional
是 Num
的子 class,因此类型 a
也必须是 Num
。因此,baz
可以同时使用 foo
和 bar
。
也就是说,因为subclass关系,签名
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
chi 的回答对正在发生的事情给出了很好的高级解释。我认为提供一种稍微低级(但也更机械)的方式来理解这一点可能也很有趣,这样您就可以解决其他类似的问题,转动曲柄并获得正确的答案。我将讨论类型作为该类型值的用户与实现者之间的一种协议。
- 对于
forall a. t
,调用者可以选择一种类型,然后他们继续使用协议t
(其中a
在t
). - 对于
Foo a => t
,调用者必须向实现者提供证据证明a
是Foo
的一个实例。然后他们继续协议t
. - 对于
t1 -> t2
,调用者可以选择类型t1
的值(例如,通过 运行 协议t1
,实现者和调用者的角色互换)。然后他们继续协议t2
. - 对于任何类型
t
(即在任何时间),实施者可以通过生成适当类型的值来缩短协议。如果上面的规则none适用(例如,如果我们已经达到像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
. 的证明中提取 valNum
的实现者然后根据需要提供类型a
的值。
Num a
的证明- 选择类型。这里她选择
有了球场上的所有细节,我们现在可以尝试缩小并查看大图。 applyIdNum
和applyIdFrac
的类型相同,即forall a. Fractional a => a
。所以在这两种情况下,实现者都假设 a
是 Fractional
的一个实例。但是由于所有 Fractional
实例都是 Num
实例,这意味着实现者可以假设 Fractional
和 Num
都适用。这使得使用在实现中假设任一约束的函数或值变得容易。
P.S。我反复使用副词 "quietly" 来选择 forall a. t
协议期间所需的类型。这是因为 Haskell 非常努力地向用户隐藏这些选择。但是如果你喜欢使用 TypeApplications
扩展名,你可以将它们显式化;在协议 f
中选择类型 t
使用语法 f @t
。不过,实例证明仍然以您的名义默默管理。
Works as expected because every
Fractional
is also aNum
.
这是正确的,但重要的是要准确说明这意味着什么。这意味着:Fractional
class 中的每个 type 也在 Num
class 中。它 不是 具有 OO 或动态背景的人可能理解的意思:“Num
类型中的每个 value 也在Fractional
类型”。如果是这种情况,那么您的推理就有意义了:那么 Num
值 bar
将不够通用,无法在 foo
函数中使用。
...或者实际上不会,因为在 OO 语言中,数字层次结构会朝另一个方向工作——其他语言通常允许您将任何数值转换为小数,但另一个方向在这些语言中会产生轮回,而强类型语言不会自动产生轮回!
在Haskell中,你需要担心none,因为从来没有任何隐式类型转换。 bar
和 foo
在完全相同的类型上工作,这种类型发生在变量 a
是次要的。现在,bar
和 foo
都以不同的方式约束这个单一类型,但是因为它是受约束的相同类型,所以您只需得到两个约束的组合 (Num a, Fractional a)
,这是由于 Num a => Fractional a
相当于单独的 Fractional a
。
TL;DR: Num a => a
不是 一个 Num
值,而是相反,它是一个定义 can 是 any 类型 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
.
这当然意味着,如果代码的其余部分没有任何其他内容,进一步指定类型,我们将得到一个不明确的类型错误(除非出现某些类型默认设置)。但是 可能 存在。现在这是可以接受的,并且有一个类型——一个 polymorphic 类型,意思是,在代码的其余部分的某个地方,在任何特定的使用地点,其他东西必须进一步指定它它出现。所以现在,作为一个通用的多态定义,这是完全可以接受的。
接下来,
> 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
(我post这个回答是因为我觉得最简单的东西对于初学者来说可能是绊脚石,而这些最简单的东西对于专家来说是理所当然的,甚至没有注意到。俗话说,我们不知道是谁发现了水,但肯定不是鱼。)