为什么 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 中。由于 FractionalNum 的子 class,因此类型 a 也必须是 Num。因此,baz 可以同时使用 foobar

也就是说,因为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(其中 at).
  • 对于Foo a => t,调用者必须向实现者提供证据证明aFoo的一个实例。然后他们继续协议 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。协议说:

  1. 来电者选择类型a
  2. 来电证明是Fractional.
  3. 实施者提供类型为 a 的值。

实施说:

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

    1. 选择类型a。她悄悄地做出了与 她的 来电者相同的选择。
    2. 证明aNum的实例。这没有问题,因为她实际上知道 aFractional,这意味着 Num.
    3. 提供类型 a 的值。在这里她选择 valFrac。为了完整起见,她必须证明 valFrac 的类型为 a.
  2. 因此,实施者现在运行 valFrac 协议。她:

    1. 选择类型a。在这里,她悄悄地选择了 idNum 期望的类型,这恰好与她的来电者为 a 选择的类型相同。
    2. 证明aFractional的实例。她使用了来电者使用的相同证据。
    3. valFrac 的实现者然后承诺根据需要提供 a 类型的值。

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

  1. 来电者选择类型a
  2. 来电证明aFractional.
  3. 实施者必须提供类型为 a 的值。

实施说:

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

    1. 选择类型。在这里,她悄悄地选择来电者选择的任何东西。
    2. 证明aFractional。她转达了来电者的证明。
    3. 选择类型 a 的值。她将执行 valNum 协议来执行此操作;并且我们必须检查这是否会产生 a.
    4. 类型的值
  2. 在执行valNum协议期间,她:

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

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

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 类型”。如果是这种情况,那么您的推理就有意义了:那么 Numbar 将不够通用,无法在 foo 函数中使用。
...或者实际上不会,因为在 OO 语言中,数字层次结构会朝另一个方向工作——其他语言通常允许您将任何数值转换为小数,但另一个方向在这些语言中会产生轮回,而强类型语言不会自动产生轮回!

在Haskell中,你需要担心none,因为从来没有任何隐式类型转换。 barfoo 在完全相同的类型上工作,这种类型发生在变量 a 是次要的。现在,barfoo 都以不同的方式约束这个单一类型,但是因为它是受约束的相同类型,所以您只需得到两个约束的组合 (Num a, Fractional a),这是由于 Num a => Fractional a 相当于单独的 Fractional a

TL;DR: Num a => a 不是 一个 Num 值,而是相反,它是一个定义 canany 类型 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.

可以接受的

因为我们现在知道 aFractional 中(因为 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这个回答是因为我觉得最简单的东西对于初学者来说可能是绊脚石,而这些最简单的东西对于专家来说是理所当然的,甚至没有注意到。俗话说,我们不知道是谁发现了,但肯定不是。)