为什么Num可以表现为小数?

has*_*lHQ 3 haskell

如预期的那样,可以正常工作:

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)

问题在评论中。你可以解释吗?

chi*_*chi 5

型的abaz :: Fractional a => a由呼叫谁选择baz。他们有责任确保自己的a类型选择在Fractional班上。由于Fractional是的子类Numa因此类型也必须也是Num。因此,baz可以同时使用foobar

换句话说,由于子类的关系,签名

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)


Dan*_*ner 5

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先谈谈。协议说:

  1. 调用者选择一种类型a
  2. 来电者证明是Fractional
  3. 实现者提供了一个类型的值a

实现说:

  1. 实现idNum者作为调用者启动协议。所以,她必须:

    1. 选择一种类型a。她静静地做一样的选择,她的来电显示一样。
    2. 证明它a是 的一个实例Num。这没有问题,因为她实际上知道那aFractional,这意味着Num
    3. 提供一个类型的值a。她在这里选择valFrac。为了完整起见,她必须然后显示valFrac具有类型a
  2. 所以实现者现在运行valFrac协议。她:

    1. 选择一个类型a。在这里,她悄悄地选择了idNum期待的类型,恰好和她的来电者选择的类型相同a
    2. 证明它a是 的一个实例Fractional。她使用与来电者相同的证据。
    3. valFracthen的实现者承诺根据需要提供 type 的值a

为了完整起见,这里是对 的类似讨论applyIdFrac。协议说:

  1. 调用者选择一种类型a
  2. 来电者证明aFractional
  3. 实施者必须提供一个 type 值a

实现说:

  1. 实施者将执行idFrac协议。所以,她必须:

    1. 选择一种类型。在这里,她静静地选择她的来电者选择的任何东西。
    2. 证明aFractional。她传递了她的来电者的证据。
    3. 选择一个类型的值a。她将执行valNum协议来做到这一点;并且我们必须检查这是否产生了一个 type 值a
  2. 在执行valNum协议期间,她:

    1. 选择一种类型。在这里她选择idFrac期望的类型,即a;这也恰好是她的来电者选择的类型。
    2. 证明Num a成立。这是她可以做的,因为她的调用者提供了 的证明Fractional a,您可以Num a从 的证明中提取 的证明Fractional a
    3. valNumthen的实现者a根据需要提供 type 的值。

有了现场的所有细节,我们现在可以尝试缩小并查看大图。两者applyIdNumapplyIdFrac具有相同的类型,即forall a. Fractional a => a。因此,两种情况下的实现者都假设这aFractional. 但由于所有Fractional实例都是Num实例,这意味着实现者可以同时假设FractionalNum应用。这使得使用在实现中假定任一约束的函数或值变得容易。

PS 我反复使用副词“安静地”来选择forall a. t协议期间所需的类型。这是因为 Haskell 非常努力地向用户隐藏这些选择。但是,如果您喜欢TypeApplications扩展名,您可以使它们明确;t在协议中选择类型f使用语法f @t。不过,实例证明仍然代表您进行静默管理。