Scala: converting a list to a map
I have an animal class defined as
case class Animal(name: String, properties: List[String])
Given a list of animals, I want a map from a property -> a list of animals that satisfy that property
As an example, if I have an input like, say
List(
Animal("Dog",
List("has tail",
"can swim",
"can bark",
"can bite")),
Animal("Tuna",
List("can swim",
"has scales",
"is edible")),
Animal("Black Mamba",
List("has scales",
"is venomous",
"can bite"))
)
The way out should be
Map(
"has tail" -> List(Dog)
"can swim" -> List(Tuna,Dog)
"can bark" -> List(Dog)
"has scales" -> List(Tuna,Snake)
"is edible" -> List(Tuna)
"is venomous" -> List(Snake)
"can bite" -> List(Dog,Snake)
)
I am new to functional programming. I can do this in an imperative manner, but struggled to come up with a functional solution. Any pointers are appreciated! :)
source to share
You want to get a list of key-value pairs to start with. We can start this problem by first seeing how we will be hiding one Animal
list of key-value pairs. You may have heard of the function map
. This allows you to transform lists and other basic structures by applying a function to each item in the list. We can use it for good effect here:
animal.properties.map(property => (property, animal.name))
Here we take an animal properties
, and for each of them use an anonymous function: property => (property, animal.name)
. This function creates a tuple (key-value pair in this case) of the property along with the animal's name.
Now we want to apply this to all animals in the list. This may sound like another one map
, but then we'll have a list of lists of tuples when we really need a list of tuples. It's when you use flatMap
that takes a method that returns a list and applies it to each element and flattens the list. So we just apply the above method to each item.
val kvps = animals.flatMap(animal => animal.properties.map(property => (property, animal.name))).toMap
We now have a list of key-value pairs. Now we want to group them by their key. The method groupBy
returns a list of tuples, where the left side is the key and the right side is a list of key-value pairs. This is almost what we want, but we just need the values on the right side. Thus, we can do:
kvps.groupBy { case (key, value) => key }.toMap.mapValues(keyValues => keyValues.map { case (key, value) => value })
In general, it might look like this:
animals.flatMap { animal =>
animal.properties map { property => (animal, property) }
}.groupBy { case (key, value) => key }.toMap mapValues { keyValues =>
keyValues map { case (key, value) => value }
}
Of course, Scala has tons of syntactic sugar that can make this method very concise:
animals.flatMap(a => a.properties.map(_ -> a.name)).groupBy(_._1).toMap.mapValues(_.map(_._2))
source to share
One such way would be the following:
animals.
flatMap(a => a.properties.map(p => (p, a.name)))
.groupBy(_._1)
.mapValues(_.map(_._2))
flatMap
gives you a list of property tuples -> Animal Name
groupBy
then grouped by property name, giving Map[String, List[(String,String)]
where key Map
is property and value is a tuple of property name -> animal name
Then it mapValues
takes the resulting List((String,String))
map values and converts them only to the second part of the tuple, which is the name of the animal
source to share
case class Animal(name: String, properties: List[String])
val animals = List(
Animal("Dog", List("has tail","can swim","can bark","can bite")),
Animal("Tuna", List("can swim", "has scales", "is edible")),
Animal("Black Mamba", List("has scales", "is venomous", "can bite"))
)
animals
.flatMap(a => a.properties.map(ab => ab -> a.name))
.groupBy(_._1)
.map(g => g._1 -> g._2.map(_._2)).toMap
The approach is as follows
- Creating tuples (property -> animal names)
- Group by property
- Map tuple (property, List ((property, name))) to (property, List (name))
source to share