4
votes

I have a nested map m which is like:

m = Map("email" -> "[email protected]", "background" -> Map("language" -> "english"))

I have an array arr = Array("background","language")

How do I foldLeft/reduce the array and find the string "english" from the map. I tried this:

arr.foldLeft(m) { (acc,x) => acc.get(x) }

But I get this error:

<console>:10: error: type mismatch;
 found   : Option[java.lang.Object]
 required: scala.collection.immutable.Map[java.lang.String,java.lang.Object]
       arr.foldLeft(m) { (acc,x) => acc.get(x) }
2
Don't have a working code, but here's a hint: m("background").asInstanceOf[Map[String,String]]("language"). m("background") is a java.lang.Object, you need an extra cast/match somewhere. However this data structure seems a bit odd and you should consider something more object oriented.Tomasz Nurkiewicz

2 Answers

12
votes

You should pay attention to types. Here, you start with m : Map[String, Any] as your acc. You combine with a string x and calls get, which returns an Option[Object]. To continue, you must check that there is a value, check whether this value is a Map, cast (unchecked because of type erasure, hence dangerous).

I believe the fault is in the that the type of your structure, Map[String, Any] represents what you have rather poorly.

Suppose you do instead

sealed trait Tree
case class Node(items: Map[String, Tree]) extends Tree
case class Leaf(s: String) extends Tree

You may add some helpers to make declaring a Tree easy

object Tree {
  implicit def fromString(s: String) = Leaf(s)
  implicit def fromNamedString(nameAndValue: (String, String)) 
    = (nameAndValue._1, Leaf(nameAndValue._2))
}
object Node {
  def apply(items: (String, Tree)*) : Node = Node(Map(items: _*))
}

Then declaring the tree is just as easy as your first version, but the type is much more precise

m = Node("email" -> "[email protected]", "background" -> Node("language" -> "english"))

You can then add methods, for instance in trait Tree

  def get(path: String*) : Option[Tree] = {
    if (path.isEmpty) Some(this)
    else this match {
      case Leaf(_) => None
      case Node(map) => map.get(path.head).flatMap(_.get(path.tail: _*))
    }
  }
  def getLeaf(path: String*): Option[String] 
    = get(path: _*).collect{case Leaf(s) =>s}

Or if you would rather do it with a fold

  def get(path: String*) = path.foldLeft[Option[Tree]](Some(this)) {
    case (Some(Node(map)), p) => map.get(p)
    case _ => None
  }
4
votes

Folding as an abstraction over nested maps isn't really supported. Also, you're approaching this in a way that is going to prevent the type system from giving you much help. But, if you insist, then you want a recursive function:

def lookup(m: Map[String,Object], a: Array[String]): Option[String] = {
  if (a.length == 0) None
  else m.get(a(0)).flatMap(_ match {
    case mm: Map[_,_] => lookup(mm.asInstanceOf[Map[String,Object]],a.tail)
    case s: String if (a.length==1) => Some(s)
    case _ => None
  })
}