3

正如预期的那样,这很好用:

foo :: Fractional a => a
foo = undefined                -- datum

bar :: Num a => a -> a
bar a = undefined              -- function

baz :: Fractional a => a
baz = bar foo                  -- application

这可以按预期工作,因为 everyFractional也是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

出乎意料地工作!有s不是NumFractionals

那么为什么我可以将参数传递给Num参数Fractional呢?你可以解释吗?

4

4 回答 4

7

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. 证明a是 的一个实例Num。这没问题,因为她其实知道那aFractional,这暗示着Num
    3. 提供类型的值a。她在这里选择valFrac。要完整,她必须然后显示valFrac具有类型a
  2. 所以实现者现在运行valFrac协议。她:

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

为了完整起见,这里是类似的讨论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. then的实现者根据需要valNum提供 type 的值a

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

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

于 2017-03-15T22:06:40.337 回答
6

输入的类型由调用 的人a选择。他们有责任保证他们选择的类型在课堂上。由于是 的子类,因此类型也必须是。因此,可以同时使用和。baz :: Fractional a => abazaFractionalFractionalNumaNumbazfoobar

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

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
于 2017-03-15T21:07:10.040 回答
2

按预期工作,因为 everyFractional也是Num.

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

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

于 2017-03-15T22:52:44.343 回答
0

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特定用途可能要求的任何类型的数据。不管是什么,既然是 in Fractional,就保证也是 in Num,所以是可以接受的numfunc

由于我们现在知道它a是 in Fractional(因为fraval),所以现在整个应用程序的类型也已知是 in 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要求其类型为 in Fractionalnumval能够提供所需的任何类型的数据,只要该类型在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

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

于 2021-09-30T13:53:45.897 回答