Why does the + operator on parameterized types in scala always result in a string
Consider the class as follows:
import scala.collection.mutable.{HashMap => MutableHashMap}
class CustomHashMap[K,V](hashMap: MutableHashMap[K,V], initVal: V) {
def addMaps(first: MutableHashMap[K, V], second: MutableHashMap[K, V]): MutableHashMap[K, V] = {
second.foreach(pair => { first += (pair._1 -> (first.getOrElse(pair._1, initVal) + pair._2)) } )
//The above line throws a compile time error
first
}
//Other functions
}
After adding two parameterized types, a compile-time error occurs saying
expected (K,V) recieved (K,String)
I want to know why scala is doing this implicit conversion? Since operator overloading is not allowed in java, it seemed logical, but in the case of scala, V could indeed be a class that could have a method for it +
.
source to share
Here's how to manually implement it:
Since you are trying to define a monoid, I will take the liberty of moving initVal
from addMaps
to an operation definition.
This can be done using the normal typeclass template in Scala, but you will need to define manually what it means +
for each type you want to use in your map.
Basically you have a trait Monoid
:
trait Monoid[T] {
def mzero: T // your initVal
def madd(first: T, second: T): T // the + operation
}
Than you define implicit implementations for each type extending this trait. You can define them in
- companion object
Monoid
, it will be used automatically, - a companion object of class T, it will also be used automatically,
- as implicit somewhere else, but you'll have to import it manually.
Here is an example Monoid
of a companion object defining implementations for strings and all kinds of numbers:
object Monoid {
implicit object StringMonoid extends Monoid[String] {
def mzero = ""
def madd(first: String, second: String) = first + second
}
implicit def NumericMonoid[T](implicit ev: Numeric[T]): Monoid[T] =
new Monoid[T] {
import Numeric.Implicits._
def mzero = ev.zero
def madd(first: T, second: T) = first + second
}
}
Then, in your function request, addMaps
specify that the map elements must be Monoid
s and use the operations provided by the monoid implementation for the elements:
def addMaps[K, V](first: MutableHashMap[K, V], second: MutableHashMap[K, V])
(implicit ev: Monoid[V]): MutableHashMap[K, V] = {
second.foreach { pair =>
first += (pair._1 -> ev.madd(first.getOrElse(pair._1, ev.mzero), pair._2)) }
first
}
And here's a test of how it works:
scala> addMaps(MutableHashMap(1 -> 2, 3 -> 4), MutableHashMap(1 -> 3, 5 -> 7))
res1: scala.collection.mutable.HashMap[Int,Int] = Map(5 -> 7, 1 -> 5, 3 -> 4)
scala> addMaps(MutableHashMap(1 -> "foo", 2 -> "bar"), MutableHashMap(1 -> "baz", 3 -> "qoo"))
res2: scala.collection.mutable.HashMap[Int,String] = Map(2 -> bar, 1 -> foobaz, 3 -> qoo)
source to share
I believe the problem is what you are trying to do V + V
and Scala infer the type V
as a string. Here is my attempt at telling the compiler what V
will extend the trait (called Adder
) that it will support V + V => V
.
import scala.collection.mutable.{HashMap => MutableHashMap}
trait Adder[A]{
def +(b: A): A
}
class CustomHashMap[K, V <: Adder[V]](hashMap: MutableHashMap[K,V], initVal: V) {
def addMaps(first: MutableHashMap[K, V], second: MutableHashMap[K, V]): MutableHashMap[K, V] = {
second.foreach(pair => { first += (pair._1 -> (first.getOrElse[V](pair._1, initVal) + pair._2)) } )
first
}
//Other functions ...
}
The above solution will require V
for extension Adder
. If we want to make it easier for the user, we can use implicit conversions. To add custom types, you need to cast the implicit conversion to scope (similar to what we did with Int, String, and Double).
import scala.collection.mutable.{HashMap => MutableHashMap}
trait Adder[A]{
def add(x:A, y: A): A
}
implicit object IntAdder extends Adder[Int] {
def add(x: Int, y: Int): Int = x + y
}
implicit object StringAdder extends Adder[String] {
def add(x: String, y: String): String = x + y
}
implicit object DoubleAdder extends Adder[Double] {
def add(x: Double, y: Double): Double = x + y
}
class CustomHashMap[K, V](hashMap: MutableHashMap[K,V], initVal: V)(implicit m: Adder[V]) {
def addMaps(first: MutableHashMap[K, V], second: MutableHashMap[K, V]): MutableHashMap[K, V] = {
second.foreach(pair => { first += (pair._1 -> m.add( first.getOrElse[V](pair._1, initVal), pair._2)) } )
first
}
//Other functions ...
}
source to share
To add to marios' answer, you can use a functional programming library that gives you class types already Semigroup
(padding) or Monoid
( padding and empty element).
For example, Cats :
scala> import cats.Monoid
scala> def joinMaps[K, V](a: Map[K, V], b: Map[K, V])(implicit m: Monoid[V]) =
b.foldLeft(a) { case (res, (k, v)) =>
res + (k -> m.combine(res.getOrElse(k, m.empty), v))
}
To check:
scala> import cats.std.list._ // has a Monoid for List
scala> joinMaps(Map("a" -> List(1, 2), "b" -> List(3, 4)),
Map("b" -> List(5, 6), "c" -> List(7, 8)))
res1: Map[String,List[Int]] = Map(a -> List(1, 2), b -> List(3, 4, 5, 6),
c -> List(7, 8))
In general, it makes your code more modular if you decouple the function to add the type to the class (here Monoid
), instead of requiring it to be part of your value type V <: Adder
.
source to share