Haskell类型替换

时间:2019-03-08 19:36:56

标签: haskell

我正在阅读Haskell书(http://haskellbook.com/)并陷入以下练习中:

f :: Float
f = 1.0

-- Question: Can you replace `f`'s signature by `f :: Num a => a`

起初,我认为答案是肯定的。浮点数为Num提供了一个实例,因此用Num a => a值代替Float应该很好(我在这里考虑协方差)。

但是这不能编译:

Could not deduce (Fractional a) arising from the literal ‘1.0’
      from the context: Num a
        bound by the type signature for:
                   f :: forall a. Num a => a
        at ...
      Possible fix:
        add (Fractional a) to the context of
          the type signature for:
            f :: forall a. Num a => a
    • In the expression: 1.0
      In an equation for ‘f’: f = 1.0

但是,如果我这样做,没问题:

f :: Fractional a => a
f = 1.0

为什么我不能在这里使用不太具体的约束,例如Num a => a

UPD

实际上,您可以将其总计为:

1.0::(Num a => a)

vs

1.0::(Fractional a => a)

为什么第二个有效,但第一个无效?我以为FractionalNum的子集(意味着FractionalNum兼容)

UPD 2:

感谢您的评论,但我仍然感到困惑。为什么有效:

f :: Num a => a -> a
f a = a

f 1.0

这不是:

f :: Num a => a
f = 1.0

UPD 3: 我刚刚发现了一些东西:

f :: Num a => a
f = (1::Int)

也不起作用。

UPD 4

据我所知,我一直在阅读所有答案/评论:

f :: Num a => a 

是Scala的

def f[A: Num]: A

这将解释为什么很多人提到a是由调用方定义的。我们可以这样写的唯一原因:

f :: Num a => a 
f = 1

是因为将1键入为Num a => a。有人可以确认这个假设吗?无论如何,谢谢大家的帮助。

5 个答案:

答案 0 :(得分:9)

如果我有f :: Num a => a,则意味着我可以在需要任何数字类型的任何地方使用f。因此,所有f :: Intf :: Double必须键入check。

在您的情况下,由于没有1.0 :: Int的原因,我们不能拥有543.24 :: Int,也就是说,Int不代表分数。但是,1.0确实适合任何分数类型(就像543.24一样)。

Fractional确实可以被视为Num的子集。但是,如果我在小数f :: forall a . Fractional a => a all 中有一个值,则不一定在所有数字类型f :: forall a . Num a => a中都有一个值。

请注意,从某种意义上说,约束位于=>的左侧,这使它们的行为相反。。即汽车是车辆的一部分,但我不能断定可以在任何汽车中使用的车轮都可以与任何车辆一起使用。相反,相反:可以在任何车辆中使用的车轮将可以在任何汽车中使用。

因此,您可以将f :: forall a . Num a => a(适合任何数字类型的值,例如35)视为f :: forall a . Fractional a => a的子类型(适合小数的值类型,例如3532.43)。

答案 1 :(得分:4)

让我们从 monomorphic 案例开始:

f :: Float
f = 1.0

在这里,您说过fFloat;不是Int,不是Double,不是任何其他类型。 1.0多态常量;它的类型为Fractional a => a,因此可以用来提供具有Fractional实例的 any 类型的值。您可以将其限制为FloatDouble等。由于f必须是Float,因此1.0才受到限制。

如果您尝试更改签名

f :: Num a => a
f = 1.0

现在您遇到了问题。现在,您已承诺f可用于提供一个具有Num实例的 any 类型的值。其中包括IntInteger,但是这些类型都没有Fractional实例。因此,编译器拒绝让您执行此操作。 1.0根本无法产生Int

同样,

1.0::(Num a => a)

是谎言,因为这不是限制;这是尝试扩展 1.0可以产生的类型集。类型为Num a => a的东西应该可以给我一个Int,但是1.0不能做到。

1.0::(Fractional a => a)

之所以起作用,是因为您只是在重述已经是真实的东西。既没有将1.0限制为较小的类型集,也没有试图扩展它。


现在我们得到了一些更有趣的东西,因为您指定的是 function 类型,而不仅仅是值类型。

f :: Num a => a -> a
f a = a

这只是说f可以将多态性不超过Num a => a的任何值作为其参数。 a可以是实现Num的任何类型,也可以是代表Num的类型的子集的多态值。 您选择了

f 1.0

,这意味着aFractional a => a统一。然后类型推断确定返回类型也是Fractional a => a,并允许返回您传入的相同值。


我们已经介绍了为什么

f :: Num a => a
f = 1.0
上面不允许

f :: Num a => a
f = (1::Int)

由于相同的原因而失败。 Int太具体了;它与Num a => a 不同

例如,(+) :: Num a => a -> a -> a需要两个相同类型的参数。所以,我可能会尝试写

1 :: Integer + f

1 :: Float + f

。在这两种情况下,我都需要f才能提供与其他参数相同类型的值,以满足(+)的类型:如果我想要一个Integer,我应该能够得到一个Integer,如果我想要一个Float,我应该能够得到一个Float。但是,如果您可以指定一个比Num a => a更具体的值,那么您将无法兑现承诺。 1.0 :: Fractional a => a无法提供Integer1 :: Int除了Int甚至无法提供Integer之外,都无法提供任何东西。


将多态类型视为从具体类型到值的函数;如果启用TypeApplications扩展名,则可以执行此操作。

Prelude> :set -XTypeApplications
Prelude> :{
Prelude| f :: Num a => a
Prelude| f = 1
Prelude| :}
Prelude> :t f
f :: Num a => a
Prelude> :t f @Int
f @Int :: Int
Prelude> f @Int
1
Prelude> :t f @Float
f @Float :: Float
Prelude> f @Float
1.0
Prelude> :t f @Rational
f @Rational :: Rational
Prelude> f @Rational
1 % 1

所有这些工作的原因是因为您承诺可以将带有Num实例的 any 类型传递给f,并且可以返回该类型的值。但是,如果您被允许说f = 1.0,那么f @Int实际上不可能返回Int,因为1.0 根本没有能力产生Int

答案 2 :(得分:2)

当涉及诸如Num a => a之类的多态类型的值时,有两个方面。值的来源和值的用途。一侧可以灵活地选择与多态类型兼容的任何特定类型(例如将Float用于Num a => a)。另一方仅限于使用无论涉及哪种特定类型均能正常工作的代码-它只能 使用对每个都有效的功能strong>类型与多态类型兼容。

没有免费的午餐;双方不能自由选择自己喜欢的兼容类型。

对于面向对象的子类多态性也是如此,但是OO多态性规则为 source 方面提供了灵活性,并将所有限制置于 use 方面(泛型除外,它的工作原理类似于参数多态性),而Haskell的参数多态性为使用方提供了灵活性,并将所有限制都置于了源方面。

例如,以通用数字类Num以及IntDouble作为其子类的OO语言,则返回类型为Num的东西的方法将以您期望的方式工作。您可以返回1.0 :: Double,但是调用者不能对Double专门提供的值使用任何方法(例如说要拆分小数部分的方法),因为调用者必须经过编程才能工作无论您返回的是Int还是Double(甚至是调用者可能不知道的代码专有的Num的任何全新子类),都一样。

Haskell中的多态性基于类型参数,而不是子类型化,而子类型化会改变周围的事物。代码中使用f :: Num a => a的位置可以自由地要求对a进行任何特定选择(受Num约束),而f的代码就是必须对值的来源进行编程,使其能够在使用场所选择的范围内工作。使用站点甚至可以随意要求使用f的代码私有的类型的值,f的实现者可能无法知道。 (我可以从字面上打开一个新文件,创建我喜欢的任何奇异的新类型,并为它Num提供一个实例,并且几年前编写的Num中多态的任何标准库函数都可以使用我的类型)

这可行:

f :: Float
f = 1.0

因为没有类型变量,所以源和使用地点都只需将其视为Float。但这不是:

f :: Num a => a
f = 1.0

因为{em>使用f的地方可以要求{em {em}的任何有效选择,并且此代码必须能够满足该要求选择。 (例如,当选择a时将不起作用,因此编译器必须拒绝Int的此定义)。但这确实可行:

f

因为现在使用站点只能自由地请求f :: Fractional a => a f = 1.0 中的任何类型,但不包括像Fractional这样的浮点文字不能支持的类型(例如Int)。 / p>

请注意,这完全是“泛型”在面向对象语言中的工作方式,因此,如果您熟悉支持泛型的任何语言,则只需像对待OO语言中的泛型类型一样对待Haskell类型即可。


可能会使您感到困惑的另一件事是:

1.0

文字f :: Float f = 1.0 实际上不是绝对的1.0。 Haskell文字比大多数其他语言的文字更加灵活。而例如Java说Float绝对是类型1.0的值(如果需要使用某些其他类型的double,则具有一些自动转换规则),在Haskell中,double实际上,它本身就是具有多态类型的东西。 1.0的类型为1.0

因此Fractional a => a定义起作用的原因是显而易见的,实际上是f :: Fractional a => a定义需要一些解释。它正好利用了我在帖子第一部分中上述的规则。您的代码f :: Float是由f = 1.0表示的值的使用站点,因此它可以要求使用任何喜欢的类型(受1.0约束)。特别是,它可以要求文字Fractional提供类型为1.0的值。

这再次证明了Float定义不能起作用的原因。这里f :: Num a => a的类型向f的呼叫者承诺,他们可以要求他们喜欢的任何类型(受f约束)。但这将通过将需求沿链向下传递给文字Num来满足该需求,该文字具有最一般的类型1.0。因此,如果Fractional a => a的使用地点要求类型为f但在Num之外的类型,则Fractional会尝试要求f提供相同的类型非1.0类型,但不能。

答案 3 :(得分:1)

名称没有类型。值具有类型,类型是值的固有部分。 :%s/verylongword/newword/g1 :: Int是两个不同的值。 Haskell不会隐式地在类型之间转换值,尽管定义采用一种类型的值并返回另一种类型的值的函数可能很简单。 (例如,1 :: Integerf :: Int -> Integer会将其f x = x的arug转换为Int

类似的声明

Integer

并不是说f :: Num a => a 的类型为f,而是说您可以将Num a => a类型的值分配给Num a => a

您可以认为像f这样的多态值是具有1 :: Num a => a实例的每种类型中所有类似1的值,包括Num1 :: Int1 :: Integer

1 :: Rational这样的赋值成功,因为文字f = 1具有预期的类型1

类似Num a => a的分配失败,因为文字f = 1.0具有不同的类型1.0,并且该类型过于具体。它不包括可能会调用Fractional a => a产生的所有类似1的值。

假设您声明了Num a => a。您可以说g :: Fractional a => a,因为类型匹配。您不能说g = 1.0,因为类型不匹配; g = (1.0 :: Float)有一个Float实例,但这只是可能有Fractional个实例的一组可能的无限类型之一。

可以Fractional,因为g = 1Fractional a => a更具体,并且有Num a => aFractional作为其超类。分配“选择” Num1 :: Num a => a重叠(并且出于所有意图和目的 1 :: Fractional a => a的子集,并将其分配给g。换句话说,1 :: Num a => a可以为具有Num实例的任何 single 类型产生值,它可以为任何 subset 产生值Num的子类隐含的类型。

答案 4 :(得分:0)

我也对此感到困惑。

我脑海中浮现的是:

// in pseudo-typescript

type Num = Int | Float;

let f: Num;
f = (1.0 as Float); // why this doesn't work

事实是,Num a => a 并不是数字类型的简单总和。 它表示可以变成各种数字类型的东西

多亏了chepner的解释,现在我可以这样说服自己了:

如果我有一个 Num a => a 那么我可以从中得到一个 Int,我也可以从中得到一个 Float,以及一个 Double... .

如果我能够将 1.1 安装到 Num a => a 中,那么我就无法安全地从 Int 派生出 1.1

表达式 1 能够绑定到 Num a => a 是因为 1 本身是一个多态常量,类型签名为 {{1} }.