C-style for loops in Scala 3 :: Sep 20, 2021
With Scala 3, we can easily implement fast C-style loops.
/** standard C-style for loop */
inline def loop[A](
inline start: A,
inline condition: A => Boolean,
inline advance: A => A
)(inline loopBody: A => Any): Unit =
var a = start
while condition(a) do
loopBody(a)
a = advance(a)
The usage looks like this.
loop(0, _ < 9, _ + 1) { i =>
println("number: " + i)
}
The generated byte-code viewed in the CFR decompiler looks perfect; a simple Java for loop.
for (int a = 0; a < 9; ++a) {
Predef$.MODULE$.println((Object)("number: " + a));
}
Lets also define some ergonomic variants:
/** Java-style for-of loop (ex, for(x : myList) {...}) */
inline def loop[A](inline iterable: Iterable[A])(inline loopBody: A => Any): Unit =
val it = iterable.iterator
while it.hasNext do
loopBody(it.next())
import java.lang.Iterable as JIterable
/** Java-style for-of loop (ex, for(x : myList) {...}) */
inline def loop[A](inline iterable: JIterable[A])(inline loopBody: A => Any): Unit =
val it = iterable.iterator
while it.hasNext do
loopBody(it.next())
/** for-of loop, for Arrays */
inline def loop[A](arr: Array[A])(inline loopBody: A => Any): Unit =
var i = 0
while i < arr.length do
loopBody(arr(i))
i += 1
/** simple loop for ranges */
inline def loop(
start: Int,
until: Int,
inline advance: Int => Int = _ + 1
)(inline loopBody: Int => Any): Unit =
var i = start
while i < until do
loopBody(i)
i = advance(i)
But why not use for-do or while? Consider an example: given a two dimensional array, return true if the array contains some integer x.
It’s easy enough with Scala’s for-do
:
def arrContains(arr: Array[Array[Int]], x: Int): Boolean =
for row <- arr do
for i <- row do
if i == x then return true
false
However if you decompile the output you’ll see a monster:
public boolean arrContains(int[][] arr, int x) {
boolean bl;
Object object = new Object();
try {
Object object2 = Predef$.MODULE$.refArrayOps((Object[])arr);
ArrayOps$.MODULE$.foreach$extension(object2, (Function1)(JProcedure1 & Serializable)row -> {
Object object = Predef$.MODULE$.intArrayOps(row);
ArrayOps$.MODULE$.foreach$extension(object, (Function1)(JFunction1.mcVI.sp & Serializable)i -> {
if (i == x) {
throw new NonLocalReturnControl(object, (Object)BoxesRunTime.boxToBoolean((boolean)true));
}
});
});
bl = false;
}
catch (NonLocalReturnControl ex) {
if (ex.key() == object) {
bl = BoxesRunTime.unboxToBoolean((Object)ex.value());
}
throw ex;
}
return bl;
}
Numbers are being boxed, Early Return is implemented by throwing Exceptions, and because x is in an outer scope, the second foreach lambda is capturing, which generates an anonymous class.
The performance is good with while
; the byte code looks exactly like the source. But it’s hard to read.
def arrContains(arr: Array[Array[Int]], x: Int): Boolean =
var row = 0
while row < arr.length do
val rowArr = arr(row)
var col = 0
while col < rowArr.size do
if rowArr(col) == x then return true
col += 1
row += 1
false
And finally, using loop():
def arrContains(arr: Array[Array[Int]], x: Int): Boolean =
loop(arr) { rowArray =>
loop(rowArray) { i =>
if i == x then return true
}
}
false
Nearly as ergonomic as for-do, and the byte code looks much cleaner:
public boolean arrContains(int[][] arr, int x) {
for (int i = 0; i < arr.length; ++i) {
int[] rowArray$proxy1 = arr[i];
for (int i2 = 0; i2 < rowArray$proxy1.length; ++i2) {
int i$proxy1 = rowArray$proxy1[i2];
if (i$proxy1 != x) continue;
return true;
}
}
return false;
}
I wonder if for-do could be updated to behave like the loop() overloads, falling back to foreach(..) when required. I’m sure it would make some codebases a lot faster.