Different types in Map Scala

后端 未结 7 1541
情书的邮戳
情书的邮戳 2020-12-14 03:11

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

相关标签:
7条回答
  • 2020-12-14 03:50

    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)
    
    0 讨论(0)
提交回复
热议问题