【问题标题】:Scala <collection>.reduce strange behaviour on generic typesScala <collection>.reduce 泛型类型的奇怪行为
【发布时间】:2019-05-25 11:45:29
【问题描述】:

我一直在想,为什么这段代码不能编译?

Scala 中有没有一种方法可以创建通用参数化的方法/函数,并允许像“reduce”这样的操作。

这种行为是否与类型擦除有任何共同之处,还是其他?我希望看到对此的广泛解释:)

def func2[B <: Int](data: Seq[B]): Unit = {
    val operation = (a: B, b: B) => a.-(b)

    data.reduce(operation)
  }

编译器说:

type mismatch;
 found   : (B, B) => Int
 required: (Int, Int) => Int

此外,本着同样的精神 - 总体上是否可以使用此方法在参数化集合上调用任何“类流”方法:

   def func2[B <: Int](data: Seq[B]): Unit = {
       val operation = (a: B, b: B) => a.-(b)

       data.sum
  }

也给出:

could not find implicit value for parameter num: Numeric[B]

【问题讨论】:

  • class Int是final,你可以创建Int的子类

标签: scala sum reduce


【解决方案1】:

a.-(b) 的结果始终为Int,而您的operation 函数为(B, B) =&gt; Int。但是reduce 需要一个(B, B) =&gt; B 函数。

def reduce[A1 >: A](op: (A1, A1) => A1): A1

因此,(Int, Int) =&gt; Int 函数是编译器的唯一选择,因为Int 结果类型为operation

这个变种编译:

def func2[B <: Int](data: Seq[B]): Unit = {
    val operation = (a: Int, b: Int) => a.-(b)
    data.reduce(operation)
}

Numeric 不是协变的。它的接口是Numeric[T]。 Hense Numeric[B] 不是Numeric[Int]B &lt;: Int 的子类,也没有隐含的Numeric[B]

【讨论】:

  • 是的,我也认为组合有效:“val operation = (a: Int, b: Int) => a.-(b)”,但我只是想知道,是不是可能不需要显式定义“刚性”类型的参数,只需在整个方法体中使用“B”...
  • 那么你不应该使用-函数。
  • 查看Int.- 定义scala-lang.org/api/2.12.3/scala/Int.html#-(x:Double):Double。没有泛型参数的定义。
  • 另外,在 Scala 中,函数是由参数类型逆变和由结果类型协变的。所以(B, B) =&gt; Int 不是(B, B) =&gt; B 的子类型(因为Int 不是B 的子类型)并且您不能将(B, B) =&gt; Int 函数作为(B, B) =&gt; B 参数传递。
  • 阿列克谢宾果游戏!这一切都归结为-的签名。此外,@dreamsComeTrue 只有当Int 中的- 的定义类似于def -(x: A): A 其中[A &lt;: Int] 时,您所期望的才有可能。但是想一想:假设它的实现者可以从Int 值中减去x(因为x 也是Int),但是它如何将Int 结果转换回A ?
【解决方案2】:

为什么我不能为集合类型设置上限,并假设 B 类型(具有该约束)只有我需要的这些方法?

你的假设是正确的。您对B 的上限进行以下编译

val operation = (a: B, b: B) => a.-(b) 

并且还使reduceSeq[B] 上可用,因为Seq 是协变的。

由于编译器知道“B ISA Int”,所以- 方法就存在于它上面。但是,它仍然会返回一个Int。因为+的签名将返回类型限制为Int

def +(x: Int): Int

reduce 操作只能理解一种类型。所以如果你有

reduce[B](operation)

它将期望operation 的类型为(B,B) =&gt; B

如果你有

reduce[Int](operation)

它将期望operation 的类型为(Int,Int) =&gt; Int

可以做的一件事是

val operation = (a: Int, b: Int) => a - b

这是安全的,因为您的 B 始终也是 Int

【讨论】:

  • This is safe because your B is always also an Int - 是的,但是将B 放在那里而不显式声明类型不是很好吗 - 一个只有通用占位符的方法体:) scalac 应该知道,它是typeish 'Int' - 来自方法声明,因此它应该足够聪明,可以在操作中解析这些类型。
【解决方案3】:

这行得通

def func2[B](data: Seq[B], f: (B, B) => B): Unit = {
  val operation = (a: B, b: B) => f(a, b)
  data.reduce(operation)
}

【讨论】:

  • 对,你是对的。但是有什么地方可以解释这种行为吗?为什么我不能为集合类型设置上限,并假设 B 类型(具有该约束)只有我需要的这些方法?
【解决方案4】:

目前还不清楚您要达到的目标。

首先限制B &lt;: Int 没有意义,因为Int 是Scala 中的final 类。

其次,将reduce- 一起使用也没有意义,因为- 不是commutative。这很重要,因为 reduce 不像 reduceLeft/reduceRightfoldLeft/foldRight 不保证评估的顺序。其实

def reduce[A1 >: A](op: (A1, A1) => A1): A1 = reduceLeft(op)

一样有效的默认实现
def reduce[A1 >: A](op: (A1, A1) => A1): A1 = reduceRight(op)

但显然它们会为- 操作产生不同的结果。

从更高级别的角度来看,使用type classes 尤其是Numeric 可以实现类似于您想要实现的目标。例如,您可以有这样的方法:

def product[B: Numeric](data: Seq[B]): B = {
  val numeric = implicitly[Numeric[B]]
  data.reduce(numeric.times)
}

请注意,乘法是可交换的,因此它是一个合理的实现。实际上这几乎就是标准库中sumproduct 的实现方式。主要区别在于实际实现使用foldLeft,它允许为空Seq(分别为0和1)定义默认值

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2021-01-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-11-07
    • 1970-01-01
    相关资源
    最近更新 更多