Abhishek Gupta
Abhishek Gupta

Reputation: 1193

How to create a generic class in Scala

I was trying to create a Generic Calculator in Scala which can work on all Integer types. When I am trying the following code it gives me an error

Error: "Type mismatch. Required: String, found: T"

class Calculator[T <: Integral[T]] {
  def add[T](a: T, b: T): T = a + a
}

Can someone please explain to me how come it was expecting String type?

Upvotes: 0

Views: 263

Answers (2)

bottaio
bottaio

Reputation: 5073

The reason why you see

Error: "Type mismatch. Required: String, found: T"

is that by default Scala 2 defines the following implicit method in Predef

implicit final class any2stringadd[A](private val self: A) extends AnyVal {
  def +(other: String): String = String.valueOf(self) + other
}

(this would not compile in Scala 3). If you wanted to write single non-generic calculator class that would work for all integral types (whatever that means for you - Scala doesn't have inheritance hierarchy that would restrict types to just Int, Byte, Long). You would use your own Integer typeclass

class Calculator {
  def add[A: Integer](lhs: A, rhs: A): A = implicitly[Integer[A]].plus(lhs, rhs)
}

with a following definition

trait Integer[A] {
  def plus(lhs: A, rhs: A): A
}

object Integer {
  implicit val Int: Integer[Int] = new Integer[Int] {
    override def plus(lhs: Int, rhs: Int): Int = lhs + rhs
  }

  // note explicit toByte cast -> byte addition can overflow! 
  // (thus result type is Int and you might want to stick 
  // to the original type in your use case)
  implicit val Byte: Integer[Byte] = new Integer[Byte] {
    override def plus(lhs: Byte, rhs: Byte): Byte = (lhs + rhs).toByte
  }
}

To make it look a bit nicer, you might leverage extension methods.

class Calculator {
  import Integer._
  def add[A: Integer](lhs: A, rhs: A): A = lhs + rhs
}

object Integer {
  implicit class IntegerOps[A: Integer](lhs: A) {
    def +(rhs: A): A = implicitly[Integer[A]].plus(lhs, rhs)
  }
  // the rest of the object stays the same
}

and with Scala 3, you can write it even more concisely

class Calculator {
  def add[A: Integer](lhs: A, rhs: A): A = lhs + rhs
}

trait Integer[A]:
  extension(a: A) def +(b: A): A
given Integer[Int] with
  extension(a: Int) def +(b: Int): Int = a + b
given Integer[Byte] with
  extension(a: Byte) def +(b: Byte): Byte = (a + b).toByte

Edit: per @Tim suggestion you can use Integral type class that is predefined. I will still leave my answer as a reference in case you wanted to implement similar typeclass by yourself.

Upvotes: 1

Tim
Tim

Reputation: 27356

To answer the specific question, the type of a + b is String because Scala has a default + that works on any object by converting both arguments to a String and concatenating them. But what this is saying is that there is no other method + on a that can be used.

The broader problem is that Numeric is not the base class of all numeric types, but rather a typeclass that defines numeric behaviour on a type T. So the class defintion should look like this:

class Calculator[T : Integral] {

This tells the compiler that you can't create an instance of Calculator[T] unless there is an implicit instance of Integral[T] visible to the compiler at the time.

So there is an instance of Integral[T] that has methods that operate on T to do the basic numeric operations (plus, times, negate etc.). The next question is, how do I get hold of this instance of Integral[T]? The answer to that is implicitly, which will return the typeclass value for a type as long as it is available (otherwise it is a compile time error).

So the expression implicitly[Integral[T]] will return an instance of Integral[T] for whatever type was used to create the Calculator. And that instance knows how to plus values of type T. Once you know this, you can write your add function:

class Calculator[T : Integral] {
  def add(a: T, b: T): T = implicitly[Integral[T]].plus(a, b)
}

[Also note that there is no type parameter on add because the type parameter is on the class, so it is not def add[T](...) but just def add(...)]

Upvotes: 3

Related Questions