如预期的那样,可以正常工作:
valFrac :: Fractional a => a
valFrac = undefined
fNum :: Num a => a -> a
fNum a = undefined
resFrac :: Fractional a => a
resFrac = fNum valFrac -- Works as expected because every
-- Fractional is also a Num.
-- So as expected, we can pass
-- a Fractional argument into
-- a Num parameter.
Run Code Online (Sandbox Code Playgroud)
另一方面,以下方法也适用。我不明白为什么。
fFrac :: Fractional a => a -> a
fFrac a = undefined
valNum :: Num a => a
valNum = undefined
valFrac :: Fractional a => a
valFrac = fFrac valNum -- Works unexpectedly! There are
-- Nums that are not Fractionals.
-- So why can I pass a Num argument
-- into a Fractional parameter?
Run Code Online (Sandbox Code Playgroud)
问题在评论中。你可以解释吗?
型的a
在baz :: Fractional a => a
由呼叫谁选择baz
。他们有责任确保自己的a
类型选择在Fractional
班上。由于Fractional
是的子类Num
,a
因此类型也必须也是Num
。因此,baz
可以同时使用foo
和bar
。
换句话说,由于子类的关系,签名
baz :: Fractional a => a
Run Code Online (Sandbox Code Playgroud)
基本上等于
baz :: (Fractional a, Num a) => a
Run Code Online (Sandbox Code Playgroud)
您的第二个示例实际上与第一个示例相同,无论哪个foo, bar
是函数而哪个是参数。您可能还会考虑:
foo :: Fractional a => a
foo = undefined
bar :: Num a => a
bar = undefined
baz :: Fractional a => a
baz = foo + bar -- Works
Run Code Online (Sandbox Code Playgroud)
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
Run Code Online (Sandbox Code Playgroud)
我们还有两个要探索的定义:
applyIdNum :: forall a. Fractional a => a
applyIdNum = idNum valFrac
applyIdFrac :: forall a. Fractional a => a
applyIdFrac = idFrac valNum
Run Code Online (Sandbox Code Playgroud)
让我们applyIdNum
先谈谈。协议说:
a
。Fractional
。a
。实现说:
实现idNum
者作为调用者启动协议。所以,她必须:
a
。她静静地做一样的选择,她的来电显示一样。a
是 的一个实例Num
。这没有问题,因为她实际上知道那a
是Fractional
,这意味着Num
。a
。她在这里选择valFrac
。为了完整起见,她必须然后显示valFrac
具有类型a
。所以实现者现在运行valFrac
协议。她:
a
。在这里,她悄悄地选择了idNum
期待的类型,恰好和她的来电者选择的类型相同a
。a
是 的一个实例Fractional
。她使用与来电者相同的证据。valFrac
then的实现者承诺根据需要提供 type 的值a
。为了完整起见,这里是对 的类似讨论applyIdFrac
。协议说:
a
。a
是Fractional
。a
。实现说:
实施者将执行idFrac
协议。所以,她必须:
a
是Fractional
。她传递了她的来电者的证据。a
。她将执行valNum
协议来做到这一点;并且我们必须检查这是否产生了一个 type 值a
。在执行valNum
协议期间,她:
idFrac
期望的类型,即a
;这也恰好是她的来电者选择的类型。Num a
成立。这是她可以做的,因为她的调用者提供了 的证明Fractional a
,您可以Num a
从 的证明中提取 的证明Fractional a
。valNum
then的实现者a
根据需要提供 type 的值。有了现场的所有细节,我们现在可以尝试缩小并查看大图。两者applyIdNum
和applyIdFrac
具有相同的类型,即forall a. Fractional a => a
。因此,两种情况下的实现者都假设这a
是Fractional
. 但由于所有Fractional
实例都是Num
实例,这意味着实现者可以同时假设Fractional
和Num
应用。这使得使用在实现中假定任一约束的函数或值变得容易。
PS 我反复使用副词“安静地”来选择forall a. t
协议期间所需的类型。这是因为 Haskell 非常努力地向用户隐藏这些选择。但是,如果您喜欢TypeApplications
扩展名,您可以使它们明确;t
在协议中选择类型f
使用语法f @t
。不过,实例证明仍然代表您进行静默管理。