5
votes

In Scala (2.10) if I ask for a List(1.0, 2) then I get a List[Double], as expected.

But...

if I ask for a Map(1.0 -> 'A', 2 -> 'B') I get a Map[AnyVal, Char]. I'd like the keys to be of type Double. Asking for Map[Double, Char](1.0 -> 'A', 2 -> 'B) gives a type mismatch on '2'.

This I find most confusing! Is it not inconsistent?

Aside: List[(Double, Char)]((1.0, 'A'), (2, 'B')).toMap gives me a Map[Double, Char] though.

2
Reproducable on 2.9.2 as well.om-nom-nom

2 Answers

6
votes

If you look into Scala's type hierarchy, you can see that the primitive types are not in a sub-type/super-type relationship. However, there is a mechanism called numeric widening which for example allows you to call a method that takes a Double argument, by passing in say an Int. The Int then is automatically "widened" to Double.

This is the reason that List(1.0, 2) gives you List[Double].

But the Map constructor takes Tuple[A, B] arguments. The numeric widening doesn't apply to higher order types, so the target type inference doesn't work for you if you mix numeric types.

case class Test[A](tup: (A, Char)*)
Test(1.0 -> 'A', 2 -> 'B') // AnyVal

Moreover, the arrow operator -> gets in your way:

Test[Double](2 -> 'B') // found: (Int, Char)  required: (Double, Char)

This is another limitation of the type inference I think. Writing a tuple a -> b is syntactic sugar for (a, b), provided by the implicit method any2ArrowAssoc on Predef. Without this indirection, if you construct the Tuple2 directly, it works:

Test[Double]((2, 'B'))

So the numeric widening still doesn't work, but at least you can enforce the type:

Map[Double, Char]((1.0, 'A'), (2, 'B'))

A final example showing numeric widening working:

def map[A, B](keys: A*)(values: B*) = Map((keys zip values): _*)
map(1.0, 2)('A', 'B') // Map[Double, Char]
3
votes

In the case of a List, without a type declaration, Scala looks at all the elements and tries to find the common type. In your case, since Int can be converted to Double, it converts your mixed List into a List[Double] promoting your one Int.

The Map constructor takes a series of 2-tuples. You would get the same behavior, if you just constructed a list of tuples:

scala> List((1, "one"), (2.0, "two.oh"))
res0: List[(AnyVal, String)] = List((1,one), (2.0,two.oh))

Tuple2[Int, String] cannot automatically be promoted to Tuple2[Double, String]. In this case, you'll need to help the compiler out a bit with a type declaration:

scala> val x: List[(Double, String)] = List((1, "one"), (2.0, "two.oh"))
x: List[(Double, String)] = List((1.0,one), (2.0,two.oh))

or

scala> val x = List[(Double, String)]((1, "one"), (2.0, "two.oh"))
x: List[(Double, String)] = List((1.0,one), (2.0,two.oh))

or in your case:

scala> val x = List[(Double, String)]((1, "one"), (2.0, "two.oh")).toMap
x: scala.collection.immutable.Map[Double,String] = Map(1.0 -> one, 2.0 -> two.oh)

For some reason, using the type declaration on Map doesn't work. Not sure why:

scala> val x = Map[Double, String](1 -> "one", 2.0 -> "two.oh")
<console>:7: error: type mismatch;
found   : (Int, String)
required: (Double, String)
   val x = Map[Double, String](1 -> "one", 2.0 -> "two.oh")