正如预期的那样,这很好用:
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.
另一方面,以下也有效。我不明白为什么。
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?
问题出在评论中。你能解释一下吗?
答案 0 :(得分:5)
a
中的baz :: Fractional 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
答案 1 :(得分:4)
chi的答案给出了一个很好的高级解释,说明了发生了什么。我认为提供一种稍微更低级别(但也更具机械性)的方式来理解这一点也可能很有趣,这样你就可以接近其他类似的问题,转动曲柄,并得到正确的答案。我将把类型作为该类型值的用户与实现者之间的一种协议进行讨论。
forall a. t
,调用者可以选择一种类型,然后他们继续使用协议t
(其中a
已被t
替换为Foo a => t
中的调用者选择1}})。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
成立。她可以这样做,因为她的来电者提供了Num a
的证明,您可以从Fractional a
的证明中提取valNum
的证明。a
的实施者会根据需要提供applyIdNum
类型的值。有了该领域的所有细节,我们现在可以尝试缩小并查看大图。 applyIdFrac
和forall a. Fractional a => a
都具有相同的类型,即a
。因此,两种情况下的实现者都假设Fractional
是Fractional
的实例。但由于所有Num
个实例都是Fractional
个实例,这意味着实施者可以假设Num
和forall a. t
都适用。这样可以很容易地使用在实现中采用约束的函数或值。
P.S。我反复使用副词"静静地"用于TypeApplications
协议期间所需类型的选择。这是因为Haskell非常努力地隐藏用户的这些选择。但是,如果您喜欢t
扩展名,则可以将它们设为明确;在协议f
中选择类型f @t
使用语法{{1}}。但是,代表您仍然会默默管理实例证明。
答案 2 :(得分:2)
按预期工作,因为每个
Fractional
也是Num
。
这是正确的,但准确地说明这意味着什么是很重要的。这意味着:Fractional
类中的每个类型也位于Num
类中。 不意味着具有OO或动态背景的人可能会理解:“Num
类型中的每个值也属于Fractional
类型”。如果是这种情况,那么您的推理就有意义了:那么Num
值bar
在foo
函数中的使用不够通用。
...实际上它不会,因为在OO语言中,数字层次结构将在另一个方向上工作 - 其他语言通常允许您将任何数值转换为小数,但另一方向将在这些语言中会出现问题,合理强类型的语言不会自动发生!
在Haskell中,您需要担心这一点,因为从不任何隐式类型转换。 bar
和foo
处理完全相同的类型,此类型发生变量a
是次要的。现在,bar
和foo
都以不同的方式约束这个单一类型,但由于它是受约束的相同类型,因此您只需得到两个约束的组合(Num a, Fractional a)
,这归因于{{1仅相当于Num a => Fractional a
。