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.
➜ 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)
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.
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 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(",")}")