为什么Num的行为类似于分数?

时间:2017-03-15 21:01:25

标签: 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.

另一方面,以下也有效。我不明白为什么。

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?

问题出在评论中。你能解释一下吗?

3 个答案:

答案 0 :(得分:5)

a中的baz :: Fractional a => a类型由调用baz的人选择。他们有责任保证a类型的选择属于Fractional类。由于FractionalNum的子类,因此类型a必须也是Num。因此,baz可以同时使用foobar

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

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,调用者必须向实施者提供证明Foot的实例。然后他们继续使用协议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。该协议说:

  1. 来电者选择类型Fractional
  2. 来电者证明它是a
  3. 实施者提供的类型为idNum
  4. 实施说:

    1. 实施者启动a协议作为呼叫者。所以,她必须:

      1. 选择类型a。她悄悄地和她的来电者做了同样的选择。
      2. 证明Numa的实例。这没问题,因为她实际上知道FractionalNum,这意味着a
      3. 提供valFrac类型的值。她在这里选择valFrac。为完成,她必须证明a的类型为valFrac
    2. 因此,实施者现在运行a协议。她:

      1. 选择类型idNum。在这里,她悄悄地选择了a所期望的类型,这恰好与她的来电者为a选择的类型相同。
      2. 证明FractionalvalFrac的实例。她使用了来电者所做的相同证明。
      3. a的实施者随后承诺根据需要提供applyIdFrac类型的值。
    3. 为了完整起见,这里是a的类似讨论。该协议说:

      1. 来电者选择类型a
      2. 来电者证明Fractionala
      3. 实施者必须提供idFrac类型的值。
      4. 实施说:

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

          1. 选择一种类型。在这里,她默默地选择了她的来电者选择的任何东西。
          2. 证明Fractionala。她传递了来电者的证据。
          3. 选择valNum类型的值。她将执行a协议来执行此操作;我们必须检查这会产生valNum类型的值。
        2. 在执行idFrac协议期间,她:

          1. 选择一种类型。在这里,她选择a期望的类型,即Num a;这也恰好是她的来电者选择的类型。
          2. 证明Fractional a成立。她可以这样做,因为她的来电者提供了Num a的证明,您可以从Fractional a的证明中提取valNum的证明。
          3. a的实施者会根据需要提供applyIdNum类型的值。
        3. 有了该领域的所有细节,我们现在可以尝试缩小并查看大图。 applyIdFracforall a. Fractional a => a都具有相同的类型,即a。因此,两种情况下的实现者都假设FractionalFractional的实例。但由于所有Num个实例都是Fractional个实例,这意味着实施者可以假设Numforall a. t都适用。这样可以很容易地使用在实现中采用约束的函数或值。

          P.S。我反复使用副词"静静地"用于TypeApplications协议期间所需类型的选择。这是因为Haskell非常努力地隐藏用户的这些选择。但是,如果您喜欢t扩展名,则可以将它们设为明确;在协议f中选择类型f @t使用语法{{1}}。但是,代表您仍然会默默管理实例证明。

答案 2 :(得分:2)

  

按预期工作,因为每个Fractional也是Num

这是正确的,但准确地说明这意味着什么是很重要的。这意味着:Fractional类中的每个类型也位于Num类中。 意味着具有OO或动态背景的人可能会理解:“Num类型中的每个也属于Fractional类型”。如果是这种情况,那么您的推理就有意义了:那么Numbarfoo函数中的使用不够通用。
...实际上它不会,因为在OO语言中,数字层次结构将在另一个方向上工作 - 其他语言通常允许您将任何数值转换为小数,但另一方向将在这些语言中会出现问题,合理强类型的语言不会自动发生!

在Haskell中,您需要担心这一点,因为从不任何隐式类型转换。 barfoo处理完全相同的类型,此类型发生变量a是次要的。现在,barfoo都以不同的方式约束这个单一类型,但由于它是受约束的相同类型,因此您只需得到两个约束的组合(Num a, Fractional a),这归因于{{1仅相当于Num a => Fractional a