1

I've been wondering, why this piece of code won't compile?

Is there a way in Scala to create method/func that is generic parametrised and allows for such operation like 'reduce'.

Is this behaviour having anything in common with type erasure or is it something else? I would love to see broad explanation of this :)

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

    data.reduce(operation)
  }

Compiler says:

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

Also, in same spirit - is it possible overall to call any 'stream-like' method, on parametrized collection with this method:

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

       data.sum
  }

also gives:

could not find implicit value for parameter num: Numeric[B]
dreamsComeTrue
  • 116
  • 1
  • 6

4 Answers4

2

The result of a.-(b) is always Int and your operation function is (B, B) => Int. But reduce expects a (B, B) => B function.

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

So an (Int, Int) => Int function is the only one option for the compiler because of Int result type of operation.

This variant compiles:

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

Numeric isn't covariant. Its interface is Numeric[T]. Hense Numeric[B] isn't subclass of Numeric[Int] for B <: Int and there is no implicit Numeric[B].

Aleksey Isachenkov
  • 1,230
  • 6
  • 16
  • Yeah, I've also figured that combination works: "val operation = (a: Int, b: Int) => a.-(b)", but I was just wondering, is it possible without need to explicitly define "rigid" types of arguments, and just use "B" all over the method body... – dreamsComeTrue Dec 27 '18 at 14:22
  • Then you shouldn't use `-` function. – Aleksey Isachenkov Dec 27 '18 at 14:28
  • 1
    Look at `Int.-` definitions https://www.scala-lang.org/api/2.12.3/scala/Int.html#-(x:Double):Double. There are no definitions with generic parameters. – Aleksey Isachenkov Dec 27 '18 at 14:37
  • 1
    Also, in Scala functions are contravariant by parameter types and covariant by result type. So `(B, B) => Int` isn't subtype of `(B, B) => B` (because `Int` isn't subtype of `B`) and you can't pass `(B, B) => Int` function as `(B, B) => B` parameter. – Aleksey Isachenkov Dec 27 '18 at 14:47
  • Aleksey Bingo! It all boils down to signature of `-`. Also, @dreamsComeTrue what you expect is only possible if the definition of `-` in `Int` were something like `def -(x: A): A` where `[A <: Int]`. But think about it : hypothetically, an implementer of this can subtract `x` from an `Int` value (because `x` is also an `Int`), but how would it convert the `Int` result back to `A`? – Niks Dec 27 '18 at 15:14
  • Theoretically (because `Int` is final and you can't inherit it), you can create something like `RichInt` (`RichInt <: Int`) with provided generic `def -(x: B): B` and rewrite your function like `def func2[B<: RichInt](data: Seq[B]): Unit` – Aleksey Isachenkov Dec 27 '18 at 15:36
2

Why I can't put upper types bounds on type of collection, and assume, that type B (with that constraint) just has these methods I need?

Your assumption is correct. Your upper bound on B makes the following compile

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

And also makes reduce available on a Seq[B], because Seq is covariant.

Since compiler knows that "B ISA Int", the - method exists on it. However, it's still going to return an Int. Because the signature of + restricts the return type to Int

def +(x: Int): Int

The reduce operation can understand only one type. So if you have

reduce[B](operation)

It will expect operation to be of type (B,B) => B

And if you have

reduce[Int](operation)

It will expect operation to be of type (Int,Int) => Int

One of the things you can do is

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

This is safe because your B is always also an Int

Niks
  • 4,802
  • 4
  • 36
  • 55
  • `This is safe because your B is always also an Int` - yeah, but wouldn't it be nice to just put `B` there and not explicitly declare type - a method body with only generic placeholders :) scalac should know that, it is of typeish 'Int' - from method declaration, so it should be smart enough to also resolve these types in operation. – dreamsComeTrue Dec 27 '18 at 14:29
0

this works

def func2[B](data: Seq[B], f: (B, B) => B): Unit = {
  val operation = (a: B, b: B) => f(a, b)
  data.reduce(operation)
}
gekomad
  • 525
  • 9
  • 17
  • Right, you're correct. But is there somewhere explanation of this behavior? Why I can't put upper types bounds on type of collection, and assume, that type B (with that constraint) just has these methods I need? – dreamsComeTrue Dec 27 '18 at 12:59
0

It is not really clear what you are trying to achieve.

First of all restriction B <: Int makes no sense as Int is a final class in Scala.

Secondly, using reduce together with - also makes no sense because - is not commutative. This is important because reduce unlike reduceLeft/reduceRight or foldLeft/foldRight does not guarantee the order of the evaluation. Actually

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

is as valid default implementation as

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

but obviously they will produce different results for - operation.

From a higher level point of view it looks like something similar to what you want to achieve can be done using type classes and particularly Numeric. For example you could have a method like this:

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

Note that multiplication is commutative so it is a reasonable implementation. Actually this is almost how sum and product are implemented in the standard library. The main difference is that the real implementation uses foldLeft which allows defining the default value for an empty Seq (0 and 1 respectively)

SergGr
  • 23,570
  • 2
  • 30
  • 51