Scala: Calling higher-order functions on Maps
15/02/2013 Leave a comment
As I’m learning Scala, I played with Maps. The subtlety of Maps has to do with the fact that they are tuples of (key,value) pairs. Let’s look at the various ways of calling higher-order functions on them.
Let’s consider a map of bears and they weights created as follows.
scala> val myMap = Map("Brown Bear" -> 635, "Grizzly Bear" -> 360, "American Black Bear" -> 270, "Polar Bear" -> 680)
myMap: scala.collection.immutable.Map[String,Int] = Map(Brown Bear -> 635, Grizzly Bear -> 360, American Black Bear -> 270, Polar Bear -> 680)
Filtering bears that weigh more than 300kg
If I want to filter bears that weigh more than 300kg, I’ll use the filter function. I can do it by passing in a function (x) => x._2 > 300.
scala> myMap.filter((x) => x._2 > 300) res0: scala.collection.immutable.Map[String,Int] = Map(Brown Bear -> 635, Grizzly Bear -> 360, Polar Bear -> 680)
I can also use a wildcard instead of defining defining the parameter x.
scala> myMap.filter(_._2 > 300) res0: scala.collection.immutable.Map[String,Int] = Map(Brown Bear -> 635, Grizzly Bear -> 360, Polar Bear -> 680)
A third way would be to use a case in order have the tuple split for us into its two parts as input to the function.
scala> myMap.filter {case (x,y) => y>300}
res2: scala.collection.immutable.Map[String,Int] = Map(Brown Bear -> 635, Grizzly Bear -> 360, Polar Bear -> 680)
As I don’t car about the first part of the tuple, I can just use a wildcard instead of (x,y).
scala> myMap.filter {case (_,y) => y>300}
res12: scala.collection.immutable.Map[String,Int] = Map(Brown Bear -> 635, Grizzly Bear -> 360, Polar Bear -> 680)
Averaging the weights of the bears
Let’s say that I want to average the weight of the various bears. To do so, I have to use foldLeft on the map and then divide the outcome by the size of the map.
scala> myMap.foldLeft (0) ((sum,v) => sum+v._2) / myMap.size res5: Int = 486
Instead of using defining the signature of the anonymous function, I can also use wildcards.
scala> myMap.foldLeft (0) (_+_._2) / myMap.size res7: Int = 486
Now I can even use the scala shorthand for foldLeft even though I’m no real fan of it.
scala> (0/:myMap) (_+_._2) / myMap.size res9: Int = 486
The shorthand is equivalent to the following as methods that end with a colon associate to the right.
scala> (myMap./:(0)) (_+_._2) / myMap.size res10: Int = 486
Actually, the easiest way can also be to directly work on the values and call the sum function on those values.
scala> myMap.values.sum / myMap.size res6: Int = 486
Averaging the weights of the bears that weigh more than 300kg
If I now want to average the weights of the bears that weight more than 300kg, I can first filter the weights that are greater than 300kg. I can do that by filtering a Map and returning a Map using filter. I can also filter the values by first calling values
Let’s first calculate the average on the filtered list of values.
scala> myMap.values.filter(_>300).sum / myMap.size res18: Int = 418
Let’s now do it by always working on a Map and using FoldLeft.
scala> myMap.filter(_._2>300).foldLeft(0)(_+_._2) / myMap.size res0: Int = 418
Final thoughts
With Scala, there are so many ways to do the same thing that in the end, it’s just a matter or taste. Nevertheless, when working in a team, it’s good practice to try to keep the style uniform throughout the code base.
This post did not aim at listing all the possible ways to implement the examples. Given the nature of Scala, I even imagine that there are more clever ways to do it.
A last consideration has to do with performance. Given the size of my example, all solutions run pretty fast. This shall no doubt be different with larger collections.