Scala: Accessing/casting entries from Map[String, Any] or better alternative - scala

With pattern matching I am extracting attributes from an AST and saving them in a Map[String, Any], because they can be Strings, Integers, Lists etc. Now I want to use the attributes in a case class. For getting the elements I wrote this method:
def getAttr(attr: Map[String, Any], key : String):Any = {
val optElem = attr.get(key) match {
case Some(elem) => elem
case _ => throw new Exception("Required Attribute " + key + " not found")
}
}
Because I always know what type every attribute value is I want to use the value like this:
case class Object1(id: String, name: String)
Object1("o1", getAttr(attrMap, "name").asInstanceOf[String])
But I get the error "scala.runtime.BoxedUnit cannot be cast to java.lang.String"
What am I doing wrong? Or is there a better way to collect and use my attributes?

Your implementation of getAttr has type Unit since you return result of assignment of value to optElem
To fix:
def getAttr(attr: Map[String, Any], key : String):Any = {
attr.get(key) match {
case Some(elem) => elem
case _ => throw new Exception("Required Attribute " + key + " not found")
}
}

As addition to #Nyavro's absolutely correct answer: to avoid calling asInstanceOf every time you use getAttr, you can add type parameter to it:
def getAttr[R](attr: Map[String, Any], key: String): R = {
val optElem = attr.get(key) match {
case Some(elem) => elem
case _ => throw new Exception("Required Attribute " + key + " not found")
}
optElem.asInstanceOf[R]
}
And then just
Object1("o1", getAttr(attrMap, "name"))

Related

Scala Generic method errors on parameter operation

I get "type mismatch; found : Int(1) required: String" error when I try to return the incremented value of the input parameter from a Scala generic method below.
I did try using the Case method for this but it did not work as well. Basically I want to decide the operation based on input Type to the method and return the calculated/modified value.
object GenericOperations {
// def increment[typ](val x:typ):typ = x match {
// case _:Int => x + 1
// case _:String => x + "x"
// }
def increment2(x:Any):Any = {
if(x.isInstanceOf[Int]) {
x+1
}
else if (x.isInstanceOf[String]) {
x + "x"
}
else {
println("No Match type")
}
}
}
I would rather use method overloading:
def increment2(x: Int) = x + 1
def increment2(x: String) = x + "x"
If you are sure you need exactly one function you may use match.
def increment2(x: Any): Any = x match {
case v: Int => v + 1
case v: String => v + "x"
case _ =>
throw new Exception("No Match type")
}
But returning Any isn't good thing as you cannot use its result without type cast
GenericOperations.increment2(3) + 3 // type mismatch
Still you may use the same match way:
def increment2[T](x: T): T = (x match {
case v: Int => v + 1
case v: String => v + "x"
case _ => throw new Exception("No Match type")
}) match {
case v: T => v
case _ => throw new Exception("Invalid increment expression result type")
}
As it has been mentioned in the comments there is also typeclass way:
//type class
trait Incrementable[T] {
def inc(x: T): T
}
//type class instance for String
implicit val incString = new Incrementable[String] {
def inc(x: String) = x + "x"
}
//type class instance for Int, single abstract method (SAM) form
implicit val incInt: Incrementable[Int] = (x: Int) => x + 1
def increment2[T: Incrementable](x: T): T = implicitly[Incrementable[T]].inc(x)
You have declared x to be of type Any. Which means you are only allowed to use the methods of Any.
You are calling x.+(1). There is no + method in Any. Therefore, you can't use +.
You should be getting an error about + not existing, but you don't, so what's happening here?
There is an implicit conversion for string concatenation, which can convert an arbitrary object into a String and then concatenate another String to it. In this case, it converts x to an any2stringadd and then tries to add 1 to it, but the any2stringadd.+ method only takes a String as its argument, and thus you get the strange error message that it is expecting a String.
You, however, are passing 1 as an argument, which is an Int not a String.
Note that any2stringadd is deprecated in Scala 2.13, so in the future you would just get an error about a non-existent method.
Note that you have tagged this question with generics and also talk about generics multiple times in the subject and the question body, yet there are no generics in your code. In fact, with generics, this problem would not exist.
See also
Scala Beginner trying to use Parameterised Type
Type parameter in scala
Maybe something like this , even though I still don't like it because the usage of getOrElse , but here you go anyway:
object GenericOperations {
// def increment[typ](val x:typ):typ = x match {
// case _:Int => x + 1
// case _:String => x + "x"
// }
def increment2(x:Any):Any = {
if(x.isInstanceOf[Int]) {
toInt(x).getOrElse(0)+1
}
else if (x.isInstanceOf[String]) {
x + "x"
}
else {
println("No Match type")
}
}
def toInt(x: Any): Option[Int] = x match {
case i: Int => Some(i)
case _ => None
}
}

Map with different types to String

I am trying to learn some functional programming in Scala.
I have this Map:
val params: Map[String, QueryMap] = Map(
"a" -> SimpleQueryVal("1"),
"b" -> ComplexQueryVal("2", "3")
)
Where QueryMap is (might not be the best approach):
sealed trait QueryMap
case class SimpleQueryVal(value: String) extends QueryMap
case class ComplexQueryVal(values: String*) extends QueryMap
My result would be having a string like query parameters: ?a=1&b=2&b=3
I tried something, but my method return an Iterator[String] even I use mkString, looks ugly and I am sure that there's a very simple way of doing it.
def paramString(queryMap: Map[String, QueryMap]) = queryMap.keys.map { key =>
val params = queryMap(key) match {
case SimpleQueryVal(x) => "%s=%s".format(key, x)
case complexQuery: ComplexQueryVal => complexQuery.values.map { value =>
"%s=%s".format(key, value)
}
}
val result: String = params match {
case s: String => s + "&"
case s: ArrayBuffer[_] => s.mkString("&")
}
result.mkString
}
I would appreciate any idea that would make me learn something for today. :)
I think the result String can be built in a simpler, more straight forward, manner.
def paramString(queryMap: Map[String, QueryMap]): String = queryMap.map{
case (k, sq: SimpleQueryVal) => s"$k=${sq.value}"
case (k, cq: ComplexQueryVal)=> cq.values.map(k + "=" + _).mkString("&")
}.mkString("&")
A little cleaner:
def paramString(queryMap: Map[String, QueryMap]) = queryMap.flatMap {
case (key, SimpleQueryVal(x)) => Seq(s"$key=$x")
case (key, ComplexQueryVal(values # _*)) => values.map {v =>
s"$key=$v"
}
}.mkString("&")
No need for ArrayBuffer or to repeat the .mkString("&").
Keep in mind that this is good for just learning. If you're actually trying to handle HTTP query string parameters, you need to URLEncode the keys and the values and there's probably better libraries for that.
Try this:
def paramString(queryMap: Map[String, QueryMap]) = {
val qParams = queryMap.keys.map { key =>
queryMap(key) match {
case SimpleQueryVal(x) => "%s=%s".format(key, x)
case complexQuery: ComplexQueryVal => complexQuery.values.map { value =>
"%s=%s".format(key, value)
}.mkString("&")
}
}
qParams.mkString("&")
}
println(paramString(params))
Here, first you get a Set[String] like a=1 or b=2&b=3. Then you simply do another .mkString("&") to concatenate them all.

Removing "eliminated by erasure" warning in Scala

I have a simple Scala function that generates a Json file from a Map[String, Any].
def mapToString(map:Map[String, Any]) : String = {
def interpret(value:Any) = {
value match {
case value if (value.isInstanceOf[String]) => "\"" + value.asInstanceOf[String] + "\""
case value if (value.isInstanceOf[Double]) => value.asInstanceOf[Double]
case value if (value.isInstanceOf[Int]) => value.asInstanceOf[Int]
case value if (value.isInstanceOf[Seq[Int]]) => value.asInstanceOf[Seq[Int]].toString.replace("List(", "[").replace(")","]")
case _ => throw new RuntimeException(s"Not supported type ${value}")
}
}
val string:StringBuilder = new StringBuilder("{\n")
map.toList.zipWithIndex foreach {
case ((key, value), index) => {
string.append(s""" "${key}": ${interpret(value)}""")
if (index != map.size - 1) string.append(",\n") else string.append("\n")
}
}
string.append("}\n")
string.toString
}
This code works fine, but it emits a warning message in the compilation.
Warning:(202, 53) non-variable type argument Int in type Seq[Int] (the underlying of Seq[Int])
is unchecked since it is eliminated by erasure
case value if (value.isInstanceOf[Seq[Int]]) =>
value.asInstanceOf[Seq[Int]].toString.replace("List(", "[").replace(")","]")
^
The line case value if (value.isInstanceOf[Seq[Int]]) causes the warning, and I tried case value #unchecked if (value.isInstanceOf[Seq[Int]]) to removed the warning, but it does not work.
How to remove the warning?
If you don't really care about the component type (and it seems you do not, as all you do is stringify it):
case value if (value.isInstanceOf[Seq[_]]) =>
value.asInstanceOf[Seq[_]].toString.replace("List(", "[").replace(")","]")
Come to think of it, you should be able to call toString on anything anyway:
case value if (value.isInstanceOf[Seq[_]]) =>
value.toString.replace("List(", "[").replace(")","]")
but instead of toString followed by messing with the String consider Seq#mkString
value.mkString("[", ",", "]")
Finally, that pattern isInstanceOf/asInstanceOf can be replaced by a match (in all the cases)
case value: Int => value // it's an Int already now, no cast needed
case value: Seq[_] => value.mkString("[", ",", "]")
You could do the following,
case value: String => ???
case value: Double => ???
case value: Int => ???
case value: Seq[Int] #unchecked => ???
or as #Thilo mentioned
case value: Seq[_] =>
This is the better code that generates no warning message (with the hints from Thilo & Ɓukasz).
def mapToString(map:Map[String, Any]) : String = {
def interpret(value:Any) = {
value match {
case value:String => "\"" + value + "\""
case value:Double => value
case value:Int => value
case value:Seq[_] => value.mkString("[",",","]")
case _ => throw new RuntimeException(s"Not supported type ${value}")
}
}
map.toList.map { case (k, v) => s""" "$k": ${interpret(v)}""" }.mkString("{\n", ",\n", "\n}\n")
}

getting right data types for values in a map

I have a
Map[String, Any]
The values in the map can be anything from Strings and Doubles to Sequences and Sequence of Maps. I want to make a function that recognizes the type of the value and returns it when the function is fed with a key.
The idea I have in mind is to make an implicit class that 'extends' the functionality of the get function in the scala map library and put it in my package file.
Something like this:
implicit class RichMap(map: Map[String, Any]) {
def getImplicit(key: String) = {
val value = map.get(key)
value match {
case None => throw NonExistentKeyException(key + "does not exist")
case Some(v) => v match {
case s: String => s
//Other case statements
}
}
}
}
I use it like this:
myMap.getImplicit(key)
As you can see, it is more of a 'Brute force'y approach. Is there a more generic/elegant approach I can use for my problem statement?
Do you want to return a type (class) of the element under key? In that case, how about
implicit class RichMap(map: Map[String, Any]) {
def getImplicit(key: String) = map.get(key).map(_.getClass)
}
or if you really wish to throw an exception:
implicit class RichMap(map: Map[String, Any]) {
def getImplicit(key: String) = {
map.get(key) match {
case None => throw NonExistentKeyException(key + "does not exist")
case Some(v) => v.getClass
}
}
}

Scala - reduce/foldLeft

I have a nested map m which is like:
m = Map("email" -> "a#b.com", "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) }
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" -> "a#b.com", "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
}
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
})
}