Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Scala: transform a collection, yielding 0..many elements on each iteration

Given a collection in Scala, I'd like to traverse this collection and for each object I'd like to emit (yield) from 0 to multiple elements that should be joined together into a new collection.

For example, I expect something like this:

val input = Range(0, 15)
val output = input.somefancymapfunction((x) => {
  if (x % 3 == 0)
    yield(s"${x}/3")
  if (x % 5 == 0)
    yield(s"${x}/5")
})

to build an output collection that will contain

(0/3, 0/5, 3/3, 5/5, 6/3, 9/3, 10/5, 12/3)

Basically, I want a superset of what filter (1 → 0..1) and map (1 → 1) allows to do: mapping (1 → 0..n).

Solutions I've tried

Imperative solutions

Obviously, it's possible to do so in non-functional maneer, like:

var output = mutable.ListBuffer()
input.foreach((x) => {
  if (x % 3 == 0)
    output += s"${x}/3"
  if (x % 5 == 0)
    output += s"${x}/5"
})

Flatmap solutions

I know of flatMap, but it again, either:

1) becomes really ugly if we're talking about arbitrary number of output elements:

val output = input.flatMap((x) => {
  val v1 = if (x % 3 == 0) {
    Some(s"${x}/3")
  } else {
    None
  }
  val v2 = if (x % 5 == 0) {
    Some(s"${x}/5")
  } else {
    None
  }
  List(v1, v2).flatten
})

2) requires usage of mutable collections inside it:

val output = input.flatMap((x) => {
  val r = ListBuffer[String]()
  if (x % 3 == 0)
    r += s"${x}/3"
  if (x % 5 == 0)
    r += s"${x}/5"
  r
})

which is actually even worse that using mutable collection from the very beginning, or

3) requires major logic overhaul:

val output = input.flatMap((x) => {
  if (x % 3 == 0) {
    if (x % 5 == 0) {
      List(s"${x}/3", s"${x}/5")
    } else {
      List(s"${x}/3")
    }
  } else if (x % 5 == 0) {
    List(s"${x}/5")
  } else {
    List()
  }
})

which is, IMHO, also looks ugly and requires duplicating the generating code.

Roll-your-own-map-function

Last, but not least, I can roll my own function of that kind:

def myMultiOutputMap[T, R](coll: TraversableOnce[T], func: (T, ListBuffer[R]) => Unit): List[R] = {
  val out = ListBuffer[R]()
  coll.foreach((x) => func.apply(x, out))
  out.toList
}

which can be used almost like I want:

val output = myMultiOutputMap[Int, String](input, (x, out) => {
  if (x % 3 == 0)
    out += s"${x}/3"
  if (x % 5 == 0)
    out += s"${x}/5"
})

Am I really overlooking something and there's no such functionality in standard Scala collection libraries?

Similar questions

This question bears some similarity to Can I yield or map one element into many in Scala? — but that question discusses 1 element → 3 elements mapping, and I want 1 element → arbitrary number of elements mapping.

Final note

Please note that this is not the question about division / divisors, such conditions are included purely for illustrative purposes.

like image 247
GreyCat Avatar asked Feb 24 '16 11:02

GreyCat


2 Answers

Rather than having a separate case for each divisor, put them in a container and iterate over them in a for comprehension:

val output = for {
  n <- input
  d <- Seq(3, 5)
  if n % d == 0
} yield s"$n/$d"

Or equivalently in a collect nested in a flatMap:

val output = input.flatMap { n =>
  Seq(3, 5).collect {
    case d if n % d == 0 => s"$n/$d"
  }
}

In the more general case where the different cases may have different logic, you can put each case in a separate partial function and iterate over the partial functions:

val output = for {
  n <- input
  f <- Seq[PartialFunction[Int, String]](
    {case x if x % 3 == 0 => s"$x/3"},
    {case x if x % 5 == 0 => s"$x/5"})
  if f.isDefinedAt(n)
} yield f(n)
like image 158
Ben Avatar answered Nov 11 '22 10:11

Ben


You can also use some functional library (e.g. scalaz) to express this:

import scalaz._, Scalaz._

def divisibleBy(byWhat: Int)(what: Int): List[String] = 
  (what % byWhat == 0).option(s"$what/$byWhat").toList

(0 to 15) flatMap (divisibleBy(3) _ |+| divisibleBy(5))

This uses the semigroup append operation |+|. For Lists this operation means a simple list concatenation. So for functions Int => List[String], this append operation will produce a function that runs both functions and appends their results.

like image 35
Kolmar Avatar answered Nov 11 '22 11:11

Kolmar