0

I am running into situation where I need to extract certain entries into corresponding lists based on some condition. Here is my code

var keys = Vector[String]()
var data = Vector[String]()

for ((k, v) <- myMap) {
  if (v.endsWith("abc")) { keys = keys :+ v }
  if (v.endsWith("xyz")) { data = data :+ v }
}

What would be the best way to implement this logic without making keys and data as var? Is there such a thing as Immutable List builder in Scala?

For example, ImmutableList.Builder in guava (Java) https://google.github.io/guava/releases/21.0/api/docs/com/google/common/collect/ImmutableList.Builder.html

4 Answers 4

1

Every Scala collection comes with append-only builder:

val keysB, dataB = Vector.newBuilder[String]

for ((k, v) <- myMap) {
  if (v.endsWith("abc")) { keysB += v }
  if (v.endsWith("xyz")) { dataB += v }
}

val keys = keysB.result()
val data = dataB.result()
Sign up to request clarification or add additional context in comments.

2 Comments

That's exactly I was looking for... not sure why it didn't come up in Google search. Thank you!
@Programmer this is fairly uncommon usage. I personally only needed it once when I was making extensions methods for stdlib collections. Very often you can get away with map/filter/collect/etc.
1

You could just partition the values as needed.

val (keys, notKeys) = myMap.values.partition(_.endsWith("abc"))
val (data, _)       = notKeys.partition(_.endsWith("xyz"))

Your keys and data collections will be List[String] instead of Vector but that's an easy mod if necessary.

Comments

0

What about using foldLeft?

val map: Map[Int, String] = Map(
    1 -> "abc",
    2 -> "xyz",
    3 -> "abcxyz",
    4 -> "xyzabc"
)

val r = map.foldLeft((Seq.empty[String], Seq.empty[String])) {
    case ((keys, data), (k, v)) =>
        if (v.endsWith("abc")) {
            (keys :+ v, data)
        }
        else if (v.endsWith("xyz")) {
            (keys, data :+ v)
        }
        else {
            (keys, data)
        }

}
r match { 
    case (keys, data) =>
        println(s"keys: $keys")
        println(s"data: $data")
}

2 Comments

This quickly becomes ugly when a) there are more ifs and b) value needs to be added to more than one collection
@OlegPyzhcov, am I solving all problems in the world using this exact piece of code? The author asked exact question, I answered solution which solves it.
-1

If you're forced to use a var or mutable collection (beyond your needs for optimization), you're probably not thinking about the problem correctly.

Suppose we had a map m:

Map(1 -> "abc", 2 -> "xyz")

Now, we can use recursion to solve this problem (and I've done it in a tail recursive form here):

  type Keys = Vector[String]
  type Data = Vector[String]
  def keyData(m: Map[Int, String]): (Keys, Data) = {
    def go(keys: Keys, data: Data, m: List[(Int, String)]): (Keys, Data) =
      m match {
        case (k, v) :: ks if v endsWith("abc") =>
          go(v +: keys, data, ks)
        case (k, v) :: ks if v endsWith("xyz") =>
          go(keys, v +: data, ks)
        case k :: ks =>
          go(keys, data, ks)
        case _ => (keys, data)
      }
    go(Vector.empty[String], Vector.empty[String], m.toList)
  }

This will take a map and produce a pair of vectors holding the string data that matches the predicates you listed. Now, suppose we wanted to abstract and partition our map elements into vectors satisfying any two predicates p: Int => Boolean or q: Int => Boolean. Then, we'd have something that looks like this:

  type Keys = Vector[String]
  type Data = Vector[String]
  def keyData(m: Map[Int, String], p: Int => Boolean, q: Int => Boolean): (Keys, Data) = {
    def go(keys: Keys, data: Data, m: List[(Int, String)]): (Keys, Data) =
      m match {
        case (k, v) :: ks if p(v) =>
          go(v +: keys, data, ks)
        case (k, v) :: ks if q(v) =>
          go(keys, v +: data, ks)
        case k :: ks =>
          go(keys, data, ks)
        case _ => (keys, data)
      }
    go(Vector.empty[String], Vector.empty[String], m.toList)
  }

Now, we can abstract this for any key and value types K and V:

def partitionMapBy[K, V](m: Map[K, V], p: V => Boolean, q: V => Boolean): (Vector[V], Vector[V]) = {
    def go(keys: Vector[V], data: Vector[V], m: List[(K, V)]): (Vector[V], Vector[V]) =
      m match {
        case (k, v) :: ks if p(v) =>
          go(v +: keys, data, ks)
        case (k, v) :: ks if q(v) =>
          go(keys, v +: data, ks)
        case k :: ks =>
          go(keys, data, ks)
        case _ => (keys, data)
      }
    go(Vector.empty[V], Vector.empty[V], m.toList)
  }

You'll notice that there's nothing fancy going on with the recursion here. This means we can use a fold to accomplish the same thing. Here's an implementation using foldLeft:

def partitionMapBy[K, V](m: Map[K, V])(p: V => Boolean)(q: V => Boolean): (Vector[V], Vector[V]) =
    m.foldLeft[(Vector[V], Vector[V])]((Vector.empty[V], Vector.empty[V])) {
      case (acc @ (keys: Vector[V], data: Vector[V]), (_, v: V)) =>
        if(p(v)) (v +: keys, data)
        else if(q(v)) (keys, v +: data)
        else acc
    }

And you can see, for m, we get the this where, if you let p be _ endsWith("abc") and q be _ endsWith("xyz"), then you'll have exactly what you want. `

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.