scalaimplicit-conversiontypeclassimplicitimplicit-parameters

How to write an implicit Numeric for a tuple


I have a scenario where I would like to call sum on a sequence of (Double, Double) tuples. Ideally I would like to do something like the following:

implicit def toTupleNumeric[T](num: Numeric[T]) = new Numeric[(T, T)] {
    def plus(x: (T, T), y: (T, T)): (T, T) = (num.plus(x._1, y._1), num.plus(x._2, y._2))
    def minus(x: (T, T), y: (T, T)): (T, T) = (num.minus(x._1, y._1), num.minus(x._2, y._2))
    def times(x: (T, T), y: (T, T)): (T, T) = (num.times(x._1, y._1), num.times(x._2, y._2))
    def negate(x: (T, T)): (T, T) = (num.negate(x._1), num.negate(x._2))
    def fromInt(x: Int): (T, T) = (num.fromInt(x), num.fromInt(x))
    def toInt(x: (T, T)): Int = num.toInt(x._1) + num.toInt(x._2)
    def toLong(x: (T, T)): Long = num.toLong(x._1) + num.toLong(x._2)
    def toFloat(x: (T, T)): Float = num.toFloat(x._1) + num.toFloat(x._2)
    def toDouble(x: (T, T)): Double = num.toDouble(x._1) + num.toDouble(x._2)
    def compare(x: (T, T), y: (T, T)): Int = num.compare(x._1, y._1) match {
        case c if c == 0 => num.compare(x._2, y._2)
        case c => c
    }
}

But when I call sum:

val seq: Seq[(Double, Double)] = ...
val sum = seq.sum

I get a compiler error:

could not find implicit value for parameter num: Numeric[(Double, Double)]

Is there a way to implement such an implicit?


Solution

  • You seem to confuse conditional implicit

    implicit def toTupleNumeric[T](implicit num: Numeric[T]): Numeric[(T, T)] = ...
    

    with implicit conversion

    implicit def toTupleNumeric[T](num: Numeric[T]): Numeric[(T, T)] = ...
    

    With the former you're specifying that the data type (T, T) (aka scala.Tuple2[T, T]) is an instance of the type class Numeric provided that T is an instance of the type class. This means that if there is an implicit of the type Numeric[T] then there is an implicit of the type Numeric[(T, T)]. In Scala 3 the type of this conditional implicit is Numeric[T] ?=> Numeric[(T, T)] (aka ContextFunction1[Numeric[T], Numeric[(T, T)]]).

    With the latter you're specifying that the data type Numeric[T] can be used where the data type Numeric[(T, T)] is expected and this function Numeric[T] => Numeric[(T, T)] (aka Function1[Numeric[T], Numeric[(T, T)]]) should be used for such transformation automatically.

    I guess you meant the former. So add implicit to the parameter (num: Numeric[T]) making it an implicit parameter (implicit num: Numeric[T]). Your code will compile then.

    You can refresh your understanding implicits in Scala:

    Understanding implicit in Scala

    What are type classes in Scala useful for?

    Implicit conversion vs. type class

    How can I chain implicits in Scala?

    Why are implicit conversion deprecated in scala?

    Can someone explain me implicit conversions in Scala?

    Scala - Implicit conversion to implicit argument

    Implicit conversion with implicit parameter

    https://docs.scala-lang.org/tour/implicit-parameters.html https://docs.scala-lang.org/tour/implicit-conversions.html

    https://docs.scala-lang.org/scala3/book/ca-contextual-abstractions-intro.html ...

    https://docs.scala-lang.org/scala3/reference/contextual/index.html ...

    You can also rewrite your definition using a context bound (: Numeric), importing implicits, extension methods aka type-class syntax (.toInt, .+(...)), and type-class materializer (Numeric.apply[T])

    import Numeric.Implicits._
    
    implicit def toTupleNumeric[T: Numeric]: Numeric[(T, T)] =
      new Numeric[(T, T)] {
        override def plus(x: (T, T), y: (T, T)): (T, T) = (x._1 + y._1, x._2 + y._2)
        override def minus(x: (T, T), y: (T, T)): (T, T) = (x._1 - y._1, x._2 - y._2)
        override def times(x: (T, T), y: (T, T)): (T, T) = (x._1 * y._1, x._2 * y._2)
        override def negate(x: (T, T)): (T, T) = (-x._1, -x._2)
        override def fromInt(x: Int): (T, T) = (Numeric[T].fromInt(x), Numeric[T].fromInt(x))
        override def toInt(x: (T, T)): Int = x._1.toInt + x._2.toInt
        override def toLong(x: (T, T)): Long = x._1.toLong + x._2.toLong
        override def toFloat(x: (T, T)): Float = x._1.toFloat + x._2.toFloat
        override def toDouble(x: (T, T)): Double = x._1.toDouble + x._2.toDouble
        override def compare(x: (T, T), y: (T, T)): Int = Numeric[T].compare(x._1, y._1) match {
          case c if c == 0 => Numeric[T].compare(x._2, y._2)
          case c => c
        }
        override def parseString(str: String): Option[(T, T)] = ???
      }
    

    What is a "context bound" in Scala?