Allow Java collections as first class citizens in for-loops

From what I can tell, there’s no semantic ambiguity as to what the user wants when they write:

val javaCollection = JavaClass.getJavaCollection()
for (javaItem <- javaCollection) {
  ...
}

Yet we burden them with import scala.jdk.CollectionConverters._ and appending .asScala in key spots, which gets especially burdensome in deeply nested collections.

This happens just often enough to be annoying, but just rarely enough that I usually have to google the precise import statement. To make matters worse, the import statement recently changed, leading to yet another dereference.

Suggestion: allow Java collections & iterators to be used natively in for loops, without the need for imports or conversions.

While there might be performance improvements to be had by making the support “deep”, I’d be fine with a shallow syntactic sugar, i.e. the compiler effectively adding the missing import & .asScala pieces.

One strategy is to exploit -Yimports.

➜ scala -Yimports:java.lang,scala,scala.Predef,scala.jdk.CollectionConverters
Welcome to Scala 2.13.1 (OpenJDK 64-Bit Server VM, Java 11.0.5).
Type in expressions for evaluation. Or try :help.

scala> implicit class cv[A, C[A] <: java.util.Collection[A]](c: C[A]) { def foreach(f: A => Unit): Unit = c.asScala.foreach(f) }
defined class cv

scala> val vs = java.util.List.of(1,2,3)
vs: java.util.List[Int] = [1, 2, 3]

scala> for (i <- vs) println(i)
1
2
3

scala> 

similarly

scala> implicit class cv[A, C[A] <: java.util.Collection[A]](c: C[A]) { def foreach(f: A => Unit): Unit = c.asScala.foreach(f); def map[B](f: A => B) = c.asScala.map(f) }
defined class cv

scala> for (i <- vs) yield(i+1)
res2: Iterable[Int] = ArrayBuffer(2, 3, 4)

Maybe scala-collection-compat would be willing to house the glue class? that would be safer than old conversions?

➜ scala -Yimports:java.lang,scala,scala.Predef,scala.jdk.CollectionConverters,scala.collection.convert.ImplicitConversions
Welcome to Scala 2.13.1 (OpenJDK 64-Bit Server VM, Java 11.0.5).
Type in expressions for evaluation. Or try :help.

scala> val vs = java.util.List.of(1,2,3)
vs: java.util.List[Int] = [1, 2, 3]

scala> for (i <- vs) yield(i+1)
                 ^
       warning: object ImplicitConversions in package convert is deprecated (since 2.13.0): Use `scala.jdk.CollectionConverters` instead
res0: scala.collection.mutable.Buffer[Int] = ArrayBuffer(2, 3, 4)

“in for-loops”

Do you mean for comprehensions? Because we do not have for loops.

And that is an important part of this. The reason the current for sugar syntax doesn’t work for java collections, is because they do not have the required methods.

Using Java collections in Scala code, shouldn’t be common and should be a conscious decision made due to some specific constraints. And precisely because of that, I disagree with the proposed solution of just inserting the asScala implicitly. First, we have learned that implicit conversions are not good; and second, in this case it is a very bad idea because you wouldn’t understand why you ended with some scala.Buffer when you started with a java.List.

5 Likes

Could we meet in the middle?

Perhaps an import along the lines of scala.collection.JavaForComprehensionSupport._ which would add the appropriate forwarders as extension methods?

1 Like

The spec literally calls them for loops.

I haven’t read it in a while. Is the usage ironic?

Edit: the tour link also calls a for comprehension a “for-loop with a yield”, which is probably confusing.

4 Likes

Uhm, I didn’t knew that the foreach version was called for loop. That is new, thanks for sharing.

Which also gives me idea, maybe it may be possible that just the foreach version to be supported for Java collections, producing a bytecode similar to what the for each version of Java does.

It should be fairly straightforward.

  implicit class JavaForLoopSupport[A](val c: java.util.Collection[A])
      extends AnyVal {
    def foreach(f: A => Unit): Unit = c.stream.forEach(x => f(x))
  }

Yep, it works

3 Likes

It looks like if you’re ok with always getting a java.util.stream.Stream[_] out of it (no real way to bind back to the input collection type, and all the useful stuff is defined on Stream[_]), you can do full support really easily:

import scala.jdk.CollectionConverters._
import java.util.stream.{Stream => JStream}

implicit class JavaCollectionForLoopSupport[A](val c: java.util.Collection[A]){
  def foreach(f: A => Unit): Unit = c.stream.forEach(x => f(x))

  def map[B](f: A => B): JStream[B] = c.stream.map(x => f(x))
  def flatMap[B](f: A => JStream[B]): JStream[B] = c.stream.flatMap(x => f(x))
  def withFilter(p: A => Boolean): JStream[A] = c.stream.filter(x => p(x))
}

implicit class JavaStreamForLoopSupport[A](val c: JStream[A]){
  def foreach(f: A => Unit): Unit = c.forEach(x => f(x))

  def map[B](f: A => B): JStream[B] = c.map(x => f(x))
  def flatMap[B](f: A => JStream[B]): JStream[B] = c.flatMap(x => f(x))
  def withFilter(p: A => Boolean): JStream[A] = c.filter(x => p(x))
}

val src: java.util.List[Int] = (0 to 10).toList.asJava

println("for loop:")
for (a <- src) {
  println(a)
}

def duplicate(i: Int): JStream[Int] = JStream.of(i, -i)

val result = for {
  a <- src
  if a % 2 == 0
  a <- duplicate(a)
} yield a

println(s"for comprehension: ${result.toArray.mkString(",")}")

scastie