Different types in Map Scala

12,839

Solution 1

This is now very straightforward in shapeless,

scala> import shapeless._ ; import syntax.singleton._ ; import record._
import shapeless._
import syntax.singleton._
import record._

scala> val map = ("double" ->> 4.0) :: ("string" ->> "foo") :: HNil
map: ... <complex type elided> ... = 4.0 :: foo :: HNil

scala> map("double")
res0: Double with shapeless.record.KeyTag[String("double")] = 4.0

scala> map("string")
res1: String with shapeless.record.KeyTag[String("string")] = foo

scala> map("double")+1.0
res2: Double = 5.0

scala> val map2 = map.updateWith("double")(_+1.0)
map2: ... <complex type elided> ... = 5.0 :: foo :: HNil

scala> map2("double")
res3: Double = 5.0

This is with shapeless 2.0.0-SNAPSHOT as of the date of this answer.

Solution 2

This is not straightforward.

The type of the value depends on the key. So the key has to carry the information about what type its value is. This is a common pattern. It is used for example in SBT (see for example SettingsKey[T]) and Shapeless Records (Example). However, in SBT the keys are a huge, complex class hierarchy of its own, and the HList in shapeless is pretty complex and also does more than you want.

So here is a small example of how you could implement this. The key knows the type, and the only way to create a Record or to get a value out of a Record is the key. We use a Map[Key, Any] internally as storage, but the casts are hidden and guaranteed to succeed. There is an operator to create records from keys, and an operator to merge records. I chose the operators so you can concatenate Records without having to use brackets.

sealed trait Record {

  def apply[T](key:Key[T]) : T

  def get[T](key:Key[T]) : Option[T]

  def ++ (that:Record) : Record
}

private class RecordImpl(private val inner:Map[Key[_], Any]) extends Record {

  def apply[T](key:Key[T]) : T = inner.apply(key).asInstanceOf[T]

  def get[T](key:Key[T]) : Option[T] = inner.get(key).asInstanceOf[Option[T]]

  def ++ (that:Record) = that match {
    case that:RecordImpl => new RecordImpl(this.inner ++ that.inner)
  }
}

final class Key[T] {
  def ~>(value:T) : Record = new RecordImpl(Map(this -> value))
}

object Key {

  def apply[T] = new Key[T]
}

Here is how you would use this. First define some keys:

val a = Key[Int]
val b = Key[String]
val c = Key[Float]

Then use them to create a record

val record = a ~> 1 ++ b ~> "abc" ++ c ~> 1.0f

When accessing the record using the keys, you will get a value of the right type back

scala> record(a)
res0: Int = 1

scala> record(b)
res1: String = abc

scala> record(c)
res2: Float = 1.0

I find this sort of data structure very useful. Sometimes you need more flexibility than a case class provides, but you don't want to resort to something completely type-unsafe like a Map[String,Any]. This is a good middle ground.


Edit: another option would be to have a map that uses a (name, type) pair as the real key internally. You have to provide both the name and the type when getting a value. If you choose the wrong type there is no entry. However this has a big potential for errors, like when you put in a byte and try to get out an int. So I think this is not a good idea.

import reflect.runtime.universe.TypeTag

class TypedMap[K](val inner:Map[(K, TypeTag[_]), Any]) extends AnyVal {
  def updated[V](key:K, value:V)(implicit tag:TypeTag[V]) = new TypedMap[K](inner + ((key, tag) -> value))

  def apply[V](key:K)(implicit tag:TypeTag[V]) = inner.apply((key, tag)).asInstanceOf[V]

  def get[V](key:K)(implicit tag:TypeTag[V]) = inner.get((key, tag)).asInstanceOf[Option[V]]
}

object TypedMap {
  def empty[K] = new TypedMap[K](Map.empty)
}

Usage:

scala> val x = TypedMap.empty[String].updated("a", 1).updated("b", "a string")
x: TypedMap[String] = TypedMap@30e1a76d

scala> x.apply[Int]("a")
res0: Int = 1

scala> x.apply[String]("b")
res1: String = a string

// this is what happens when you try to get something out with the wrong type.
scala> x.apply[Int]("b")
java.util.NoSuchElementException: key not found: (b,Int)

Solution 3

I finally found my own solution, which worked best in my case:

case class Container[+T](element: T) {
    def get[T]: T = {
        element.asInstanceOf[T]
    }
}

val map: Map[String, Container[Any]] = Map("a" -> Container[Double](4.0), "b" -> Container[String]("test"))
val double: Double = map.apply("a").get[Double]
val string: String = map.apply("b").get[String]

Solution 4

(a) Scala containers don't track type information for what's placed inside them, and

(b) the return "type" for an apply/get method with a simple String parameter/key is going to be static for a given instance of the object the method is to be applied to.

This feels very much like a design decision that needs to be rethought.

Solution 5

I don't think there's a way to get bare map.apply() to do what you'd want. As the other answers suggest, some sort of container class will be necessary. Here's an example that restricts the values to be only certain types (String, Double, Int, in this case):

sealed trait MapVal
case class StringMapVal(value: String) extends MapVal
case class DoubleMapVal(value: Double) extends MapVal
case class IntMapVal(value: Int) extends MapVal

val myMap: Map[String, MapVal] =                                                               
  Map("key1" -> StringMapVal("value1"),
      "key2" -> DoubleMapVal(3.14),
      "key3" -> IntMapVal(42))

myMap.keys.foreach { k =>
  val message =
    myMap(k) match { // map.apply() in your example code
      case StringMapVal(x) => "string: %s".format(x)
      case DoubleMapVal(x) => "double: %.2f".format(x)
      case IntMapVal(x) => "int: %d".format(x)
    }
  println(message)
}

The main benefit of the sealted trait is compile-time checking for non-exhaustive matches in pattern matching.

I also like this approach because it's relatively simple by Scala standards. You can go off into the weeds for something more robust, but in my opinion you're into diminishing returns pretty quickly.

Share:
12,839

Related videos on Youtube

pichsenmeister
Author by

pichsenmeister

Updated on September 14, 2022

Comments

  • pichsenmeister
    pichsenmeister over 1 year

    I need a Map where I put different types of values (Double, String, Int,...) in it, key can be String.

    Is there a way to do this, so that I get the correct type with map.apply(k) like

    val map: Map[String, SomeType] = Map()
    val d: Double = map.apply("double")
    val str: String = map.apply("string")
    

    I already tried it with a generic type

    class Container[T](element: T) {
        def get: T = element
    }
    
    val d: Container[Double] = new Container(4.0)
    val str: Container[String] = new Container("string")
    val m: Map[String, Container] = Map("double" -> d, "string" -> str)
    

    but it's not possible since Container takes an parameter. Is there any solution to this?

  • pichsenmeister
    pichsenmeister almost 11 years
    I already tried this. But if I specify the Cointainer to be Any, I get an Any value on map.apply(k), so e.g. val d: Double = map.apply("double") is not possible.
  • pichsenmeister
    pichsenmeister almost 11 years
    this is a nice pattern. unfortunately you have to type-annotate the apply method. anyway, I think it's the best solution so far. thanks for your answer!
  • Rüdiger Klaehn
    Rüdiger Klaehn almost 11 years
    You mean the second one? Please be aware that by using a ClassTag, you will have problems when you put in a scala type that does not have a JVM equivalent. You can for example put in a List[Int] and ask for a List[String], and you will get out something because List[Int] and List[String] do have the same erasure. If you want it to work in this case as well, you will have to use a TypeTag instead, which captures the entire scala type and not just the JVM type. I updated the example accordingly.
  • Rüdiger Klaehn
    Rüdiger Klaehn over 10 years
    That's pretty neat. But what are the access characteristics when using a HList as a map? Since it is basically a single-linked list, you have to check each element, so lookup would be O(N), right? Also, doesn't this create awfully complex types once you have more than a few elements?
  • Egregore
    Egregore over 8 years
    Your first pattern is a very useful answer if one doesn't need all the generic magic of shapeless, but still needs a container with multiple types of values, thanks a lot!