I'm currently trying to convert some Java code to Scala code. The challenge is to ensure that the converted Scala code doesn't end up doing something very inefficient when compared to the original Java one. For e.g. when trying to convert the following code:
class Person {
String name;
Integer age;
Character gender;
}
public class TestJava {
public static void main(String[] args) {
final List<Person> persons = new ArrayList<>();
final List<Person> males = new ArrayList<>();
final List<Person> aNames = new ArrayList<>();
final List<Person> seniors = new ArrayList<>();
for (final Person p: persons) {
if (p.gender == 'm') {
males.add(p);
}
if (p.age >= 60) {
seniors.add(p);
}
if (p.name.startsWith("a")) {
aNames.add(p);
}
}
}
}
Since Java relies on mutation, this code looks logical. But, now I want to convert this to its Scala equivalent without looping over the collection multiple times (3 times in this case).
I can of course use mutable List
from the Scala library and achieve the same thing as done by Java but was wondering if it was possible to generate multiple collections from a given sequence/collection in a functional/Scala way without iterating over the collection for n
times where n
is the criteria count. Thanks in advance!
One purely functional and immutable way would be to have a generic function that separates collections into buckets, by predicate:
case class Person(name: String, age: Int, gender: String)
def bucketsByPredicate(people: Seq[Person], predicates: Seq[Person => Boolean]) = {
people.foldLeft(predicates.map(predicate =>
(predicate, List.empty[Person])
)) { case (predicates, person) =>
predicates.map { case (predicate, members) =>
(predicate, if(predicate(person)) person :: members else members)
}
}.map(_._2)
}
Then an example usage could be:
val olderThan60 = (p: Person) => p.age >= 60
val male = (p: Person) => p.gender == "m"
val Seq(olderThan60People, malePeople) = bucketsByPredicate(people, Seq(olderThan60, male))
This is fairly pragmatic. Pattern match over the list of persons, check the conditions in each recursive call and add to the results as necessary until the list is exhausted. The results are a List
of Person
that gets stuffed into a Tuple
.
class Person(val name: String, val age: Integer, val gender: Character){
override def toString = name + " " + age + " " + gender
}
val alice = new Person("Alice", 18, 'f')
val bob = new Person("Bob", 18, 'm')
val charlie = new Person("Charlie", 60, 'm')
val diane = new Person("Diane", 65, 'f')
val fred = new Person("Fred", 65, 'm')
def filterPersons(persons: List[Person]) = {
import scala.collection.mutable.{ListBuffer => LB}
def filterPersonsR(persons: List[Person], males: LB[Person], anames: LB[Person], seniors: LB[Person]): Tuple3[LB[Person], LB[Person], LB[Person]] = persons match {
case Nil => (males, anames, seniors)
case h :: t => {
filterPersonsR(t, if(h.gender == 'm') males += h else males, if(h.name.startsWith("A")) anames += h else anames, if(h.age >= 60) seniors += h else seniors)
}
}
filterPersonsR(persons, LB(), LB(), LB())
}
Testing...
scala> filterPersons(List(alice, bob, charlie, diane, fred))
res25: (scala.collection.mutable.ListBuffer[Person], scala.collection.mutable.ListBuffer[Person], scala.collection.mutable.ListBuffer[Person]) = (ListBuffer(Bob 18 m, Charlie 60 m, Fred 65 m),ListBuffer(Alice 18 f),ListBuffer(Charlie 60 m, Diane 65 f, Fred 65 m))
scala> res25._1
res26: scala.collection.mutable.ListBuffer[Person] = ListBuffer(Bob 18 m, Charlie 60 m, Fred 65 m)
scala> res25._2
res27: scala.collection.mutable.ListBuffer[Person] = ListBuffer(Alice 18 f)
scala> res25._3
res28: scala.collection.mutable.ListBuffer[Person] = ListBuffer(Charlie 60 m, Diane 65 f, Fred 65 m)
case class Person(gender:Sex,name:String,age:Int)
sealed trait Sex
case object Male extends Sex
case object Female extends Sex
def part3(l:List[Person]) = {
def aux(l:List[Person],acc:(List[Person],List[Person],List[Person])) : (List[Person],List[Person],List[Person]) = l match {
case Nil => acc
case head::tail => {
val newAcc = (if (head.gender == Male) head :: acc._1 else acc._1,
if (head.age >= 60) head :: acc._2 else acc._2,
if (head.name startsWith "a") head :: acc._3 else acc._3)
aux(tail,newAcc)
}
}
val emptyTuple = (List[Person](),List[Person](),List[Person]())
// It is (much) faster to reverse the input list and then prepend during to recursive loop.
// prepend is O(1), append is O(n)
aux(l.reverse,emptyTuple)
}
val (males,seniors,aNames) = part3(List(Person(Male,"abc",20),Person(Female,"def",61),Person(Male,"Nope",99)))
println(s"males : $males \nseniors : $seniors \naNames : $aNames")
// outputs
// males : List(Person(Male,abc,20), Person(Male,Nope,99))
// seniors : List(Person(Female,def,61), Person(Male,Nope,99))
// aNames : List(Person(Male,abc,20))
(The List[Person] tuple makes this look quite ugly, you may want to define a type alias for your results.)
import scala.collection.immutable.List
import scala.collection.mutable.ListBuffer
case class Person(name: String, age: Int, gender: String)
def partition(persons: List[Person], predicates: List[Person => Boolean]): List[List[Person]] = {
val bufs = List.fill(predicates.size)(new ListBuffer[Person])
persons.foreach { person =>
(0 until predicates.size).foreach{ i =>
if (predicates(i)(person)) bufs(i) += person
}
}
bufs map (_.toList)
}
val alice = new Person("Alice", 18, "f")
val bob = new Person("Bob", 18, "m")
val charlie = new Person("Charlie", 60, "m")
val diane = new Person("Diane", 65, "f")
val fred = new Person("Fred", 65, "m")
val olderThan60 = (p: Person) => p.age >= 60
val male = (p: Person) => p.gender == "m"
val nameStartsWith = (p: Person) => p.name.startsWith("A")
println(partition(List(alice, bob, charlie, diane, fred), List(olderThan60, male, nameStartsWith)))
this solution is not purely functional, but more straightforward. there are many situations where mutable collections(like ListBuffer) work better, just make sure you do not leak the mutable state outside the function or class. the benifits of readability will make the loss of purity worth it.
Another example using foldLeft, but maybe a bit easier to read.
//setup test data
case class Person(gender: Char, age: Int, name: String)
val persons = List(Person('m', 30, "Steve"), Person('m', 15, "John"), Person('f', 50, "Linda"))
//function that takes a list, a person and a predicate.
//returning same list if predicate is false, else new list with person added
def inBucket(f: Person => Boolean, p: Person, list: List[Person]) = if (f(p)) p :: list else list
//what to do in each fold step. produces a new intermediate tuple of lists every time
def bucketize(lists: (List[Person], List[Person], List[Person]), next: Person): (List[Person], List[Person], List[Person]) = {
val (males, females, adults) = lists;
( inBucket(_.gender == 'm', next, males),
inBucket(_.gender == 'f', next, females),
inBucket(_.age >= 18, next, adults) )
}
val (males, females, adults) = persons.foldLeft( (List[Person](), List[Person](), List[Person]()) )(bucketize)
//you can use males, females and adults now, they are of type List[Person]
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With