Scala map containing mix type values

2020-02-12 08:41发布

问题:

I have a function that returns ( groovy code)

[words: "one two", row: 23, col: 45]

In scala I change above to scala Map but then I am forced to declare it as

Map[String, Any]

but this has the disadvantage that if I access a key such as map("words") I have to add the boilerplate

map2("words").asInstanceOf[String]

Is there a better way in scala that does not require me to add asInstanceOf?

回答1:

In order to avoid the casting and benefit from static-typing, you can either return a tuple (String, Int, Int):

def getResult = ("one two", 23, 45)

val res = getResult
res._1 // the line

// alternatively use the extractor
val (line, row, _) = getResult // col is discarded
line // the line
row  // the row

or use a case class for the result:

case class MyResult(line: String, row: Int, col: Int)

def getResult = MyResult("one two", 23, 45)

val res = getResult
res.line // the line

// alternatively use the extractor provided by the case class
val MyResult(line, row, _) = getResult // col is discarded
line // the line
row  // the row

I would prefer the case class because the fields are named and it's really just one line more.



回答2:

This is where case classes are your friend. If your map's keys are case classes, then you can force client code to correctly handle the types (and force it to correctly handle all of the types).

        S:\>scala
    Welcome to Scala version 2.9.0.final (Java HotSpot(TM) 64-Bit Server VM, Java 1.6.0_25).
    Type in expressions to have them evaluated.
    Type :help for more information.

    scala> sealed abstract class Thing;
    defined class Thing

    scala> case class Toaster(slices: Int) extends Thing;
    defined class Toaster

    scala> case class Bucket(contents: String) extends Thing;
    defined class Bucket

    scala> val things = Map("toasty" -> Toaster(2), "buck" -> Bucket("stuff"));
things: scala.collection.immutable.Map[java.lang.String,Product with Serializable with Thing] = Map(toasty -> Toaster(2), buck -> Bucket(stu
ff))

scala> for (x <- things) x match {
     case (k,Toaster(s)) => println(k + " " + s)
     case (k,Bucket(c)) => println(k + " " + c)
     }
toasty 2
buck stuff

The key here is that the match statement is cracking out the various cases, and providing you with properly typed variables to match the fields within. By declaring that the abstract class as sealed, you are letting the compiler know that it has all of the available subclasses. With that information it can tell you when you have missing cases, and it could also do further optimization.



回答3:

Edit: I had understood that you got that map as is and needed a way to interface more cleanly with it. The case-class approach is, if applicable, superior to what I'm proposing below, of course.


If your keys are always mapped to the same value types, you could do something like this:

class TypedKey[T] {
  def name = {
    // assumes you declare only `object` instances
    val simpleName = getClass.getSimpleName
    val moduleName = if (simpleName.endsWith("$")) simpleName.substring(0, simpleName.size - 1) else simpleName
    val lastDollar = moduleName.lastIndexOf('$')
    if (lastDollar == -1) moduleName else moduleName.substring(lastDollar + 1)
  }
}

object RubyKeys {
  object words extends TypedKey[String]
  object row extends TypedKey[Int]
  object col extends TypedKey[Int]
}

class MapWrapper(val underlying: Map[String, Any]) {
  def apply[T](key: TypedKey[T]) = underlying(key.name).asInstanceOf[T]
}

def main(args: Array[String]) {

  val map = Map("words" -> "one two", "row" -> 23, "col" -> 45)
  val wrapper = new MapWrapper(map)

  import RubyKeys._

  val w = wrapper(words) // String
  val r = wrapper(row)   // Int
  val c = wrapper(col)   // Int
  println(w, r, c)
}


回答4:

Having only two possible value types as in your example would allow to use the Either type with the sub-types Left and Right:

val m = Map("words" -> Left("one two"), "rows"-> Right(23), "cols"-> Right(45))

If you get back a value from the map, you can check what you have, e.g. with pattern matching or using isLeft and isRight, and "unwrap" it accordingly.



标签: scala