Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Different types in Map Scala

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?

like image 336
pichsenmeister Avatar asked Jul 16 '13 18:07

pichsenmeister


1 Answers

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)
like image 188
Rüdiger Klaehn Avatar answered Oct 22 '22 14:10

Rüdiger Klaehn