为什么 Num 可以像分数一样行事



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

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参数中呢?你能解释一下吗?

Chi的回答对正在发生的事情给出了很好的高层次解释。我认为给出一种稍微低级(但也更机械)的方式来理解这一点也可能很有趣,这样你就可以处理其他类似的问题,转动曲柄,得到正确的答案。我将讨论类型作为该类型值的用户和实现者之间的一种协议。

  • 对于forall a. t,调用方可以选择一种类型,然后他们继续协议t(其中a已被替换为t中任何地方的调用方选择)。
  • 对于Foo a => t,调用方必须向实现者提供证明aFoo的实例。然后他们继续协议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。协议说:

  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类型的值 .
  2. 在执行valNum协议期间,她:

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

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

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

baz :: Fractional a => 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

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

这是正确的,但重要的是要准确理解这意味着什么。这意味着:Fractional类中的每个类型也属于Num类。这并不意味着具有OO或动态背景的人可能会理解:"Num类型中的每个也属于Fractional类型"。如果是这种情况,那么您的推理将是有意义的:那么Numbar将不够通用,无法在foo函数中使用。
。或者实际上不会,因为在 OO 语言中,数字层次结构将在另一个方向上工作——其他语言通常允许您将任何数值转换为小数,但另一个方向在这些语言中会产生 round,这合理的强类型不会自动做到!

在 Haskell 中,您不需要担心这些,因为永远不会有任何隐式类型转换。barfoo处理完全相同的类型,此类型发生在变量a是次要的。现在,barfoo都以不同的方式约束此单一类型,但由于它是受约束的同一类型,因此您只需获得两种约束的组合(Num a, Fractional a),由于Num a => Fractional a,这相当于单独Fractional a

TL;DR:并不是说Num a => a是一个Num值,而是一个定义,可以是任何类型的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.

这当然意味着,如果代码的其余部分没有其他内容,进一步指定类型,我们将得到一个模棱两可的类型错误(除非某些类型默认启动)。但可能有。目前这是可以接受的,并且有一个类型 - 多类型,这意味着,其他东西必须在代码的其余部分,在任何特定的使用站点上进一步指定它。所以就目前而言,作为一个一般的多态定义,这是完全可以接受的。

<小时 />

下一页,

> 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

(我发布这个答案是因为我认为最简单的事情可能是初学者的绊脚石,而这些最简单的事情可以被专家在不知不觉中视为理所当然。俗话说,我们不知道是谁发现了,但肯定不是

最新更新