This one also throws an exception and it doesn’t use return
. I don’t really understand why.
BTW what is the meaning of the error? java.lang.ExceptionInInitializerError
This one also throws an exception and it doesn’t use return
. I don’t really understand why.
BTW what is the meaning of the error? java.lang.ExceptionInInitializerError
But returning/throwReturn
is still brittle, right?, as the throwReturn
may not be lexically within the returning
. It doesn’t seem to provide a way for the programmer to return from any of several concentric returning
calls. — a problem which continuations are intended to solve.
You’ve just emulated non-local returns. The mechanics and problem are the same. process
returns before elements were mapped (because LazyList is lazy, i.e. element are computed on first usage) and actual mapping happens elsewhere, in this case during .mkString
.
returning
/throwReturn
is exactly what non-local returns are today, just written explicitly. I’ll show a disassembly soon to show what’s going on.
I’m sorry I don’t know what this means. What are non-local returns today? Are you talking about return
or about throw
/catch
?
Source code in Scala:
class Demo {
def withLocalReturn(nameOpt: Option[String]): String = {
nameOpt match {
case Some(name) => return s"Hello, $name!"
case None => "Hello!"
}
}
def withNonLocalReturn(nameOpt: Option[String]): String = {
nameOpt.map { name =>
return s"Hello, $name!"
}.getOrElse("Hello!")
}
}
After compilation to *.class file and decompilation to *.java code IntelliJ shows that:
//decompiled from Demo.class
import java.lang.invoke.SerializedLambda;
import scala.MatchError;
import scala.Option;
import scala.Some;
import scala.None.;
import scala.reflect.ScalaSignature;
import scala.runtime.NonLocalReturnControl;
@ScalaSignature(
bytes = "\u0006\u0005!2A\u0001B\u0003\u0001\u0011!)q\u0002\u0001C\u0001!!)1\u0003\u0001C\u0001)!)Q\u0005\u0001C\u0001M\t!A)Z7p\u0015\u00051\u0011a\u0002\u001ff[B$\u0018PP\u0002\u0001'\t\u0001\u0011\u0002\u0005\u0002\u000b\u001b5\t1BC\u0001\r\u0003\u0015\u00198-\u00197b\u0013\tq1B\u0001\u0004B]f\u0014VMZ\u0001\u0007y%t\u0017\u000e\u001e \u0015\u0003E\u0001\"A\u0005\u0001\u000e\u0003\u0015\tqb^5uQ2{7-\u00197SKR,(O\u001c\u000b\u0003+\u0001\u0002\"AF\u000f\u000f\u0005]Y\u0002C\u0001\r\f\u001b\u0005I\"B\u0001\u000e\b\u0003\u0019a$o\\8u}%\u0011AdC\u0001\u0007!J,G-\u001a4\n\u0005yy\"AB*ue&twM\u0003\u0002\u001d\u0017!)\u0011E\u0001a\u0001E\u00059a.Y7f\u001fB$\bc\u0001\u0006$+%\u0011Ae\u0003\u0002\u0007\u001fB$\u0018n\u001c8\u0002%]LG\u000f\u001b(p]2{7-\u00197SKR,(O\u001c\u000b\u0003+\u001dBQ!I\u0002A\u0002\t\u0002"
)
public class Demo {
public String withLocalReturn(final Option nameOpt) {
if (nameOpt instanceof Some) {
Some var4 = (Some)nameOpt;
String name = (String)var4.value();
return (new StringBuilder(8)).append("Hello, ").append(name).append("!").toString();
} else if (.MODULE$.equals(nameOpt)) {
String var2 = "Hello!";
return var2;
} else {
throw new MatchError(nameOpt);
}
}
public String withNonLocalReturn(final Option nameOpt) {
Object var2 = new Object();
String var10000;
try {
var10000 = (String)nameOpt.map((name) -> {
throw new NonLocalReturnControl(var2, (new StringBuilder(8)).append("Hello, ").append(name).append("!").toString());
}).getOrElse(() -> {
return "Hello!";
});
} catch (NonLocalReturnControl var4) {
if (var4.key() != var2) {
throw var4;
}
var10000 = (String)var4.value();
}
return var10000;
}
// $FF: synthetic method
private static Object $deserializeLambda$(SerializedLambda var0) {
return Class.lambdaDeserialize<invokedynamic>(var0);
}
}
As you see, the withNonLocalReturn
contains try
/catch
with throw
inside a lambda. If you move that lambda elsewhere then returning exception won’t be caught by that try
/catch
mechanism as it only works when the exception is thown during execution of that try
/catch
block.
This example is indeed enlightening. I admit, I don’t understand why it doesn’t work.
In my code, when the instance of the NonLocalExit
class is constructed, what is the value of body
which initializes the ident
slot of the instance? Why doesn’t the catch
catch it?
Perhaps in light of your recent post, perhaps the problem is that the exception gets through outside the dynamic extent of the catch
???
Here’s even simpler example:
object Demo {
def main(args: Array[String]): Unit = {
val fn = {
try { // try catches exceptions thrown during its evaluation
// but here we're returning a function without evaluating it
() => throw new Exception("my exception")
} catch {
case e: Exception =>
println(s"Exception caught: $e")
() => ()
}
}
// here we run the function with a `throw` inside
// but we aren't in a try/catch block, so we get unhandled exception
fn()
}
}
If you look closely then you’ll see that it’s exactly the same mechanism as visible in the decompiled code in my post above.
I don’t understand. You return a function which when called will throw an exception within a try
/catch
. Right? Or am I misreading it?
Edited: Oh I see, the function returned is not the { try throw} rather it is simply the () => throw...
. yes so when it is called the exception is thrown outside a try
.
Yes. Or maybe yet another explanation. Here’s how non-local returns are desugared to local returns according to the decompiled code:
Original code:
def method(as: Seq[String]): String = {
as.foreach { a =>
if (a.size == 3) return a // non-local return
}
"nope"
}
After desugaring:
def method(as: Seq[String]): String = {
val marker = ???
try {
as.foreach { a =>
if (a.size == 3) throw new NonLocalReturnControl(marker, a)
}
} catch {
case e: NonLocalReturnControl if e.key == marker =>
return e.value // local return
}
"nope"
}
So do I understand correctly now that the reason this doesnt work, is because when seq
is a lazy list, then seq.map
doesn’t iterate, but rather returns a function which will iterate later. And at that later point in time, the dynamic extent of block
has finished.
It’s related to how scastie works. It wraps a worksheet into an object and moves the initialization code to static initilalizer. When that fails the aforementioned exception is thrown by JVM.
lazyList.map(fn)
doesn’t run fn
over lazyList
elements until it’s forced. That’s the point of lazyList
. Example:
LazyList(1, 2, 3).map(_ => sys.error("boom")) // doesn't throw, will throw when you run e.g. .mkString
Seq(1, 2, 3).map(_ => sys.error("boom")) // throws immediately
yes, makes much more sense now. Thanks for the explanation.
Above, I mentioned a function I’d like to have called cousinOfFind
. Does that concept suffer from the same problem as non-local exits?
I think writing such a function would be pretty difficult, as I can’t write a function which takes any argument for which find
can be called on.
The following works (I think) if the input is of type Seq
, but doesn’t work on Set
for example. And what about some input such as an iterator which I cannot read the same value from multiple times.
val data = Seq(1,2,3,2,3,4,3,4,5,4,5,6)
def f(x:Int):Int = {
println(s"x=$x y=${2*x+3}")
2*x+3
}
def cousinOfFind[A,B](seq:Seq[A],f:A=>B,pred:B=>Boolean):Option[(A,B)] = {
val v = seq.view
v.zip(v.map(f)).find{case (_,y) => pred(y)}
}
cousinOfFind(data,
f,
(x => x == 11):Int=>Boolean)
cousinOfFind
seems strict and synchronous because it returns a tuple (which is a strict and synchronous data structure) of ordinary values. Therefore non-local returns should work inside it if you use them in strict and synchronous transformations. Let’s say you can do:
def cousinOfFind[A, B](seq:Findable[A], f:A => B, predicate: B => Boolean): (A, B) = {
// foreach method forces computation of elements of lazy collections
// and doesn't delay any extra computations
// so it's safe here even for LazyList
seq.foreach { a =>
val b = f(a)
if (predicate(b)) return a -> b
}
sys.error("no element found")
}
Yea, it should probably return an Option[(A,B)]
. I noticed that after trying to implement it.
Yes. A saner variant then:
def cousinOfFind[A, B](seq:Findable[A], f:A => B, predicate: B => Boolean): Option[(A, B)] = {
// foreach method forces computation of elements of lazy collections
// and doesn't delay any extra computations
// so it's safe here even for LazyList
seq.foreach { a =>
val b = f(a)
if (predicate(b)) return Some(a -> b)
}
None
}
Option
is also strict and synchronous, thus it’s safe to use with non-local returns.
is Findable
a real class? I just made it up.
Perhaps a bit cleaner.
def cousinOfFind[A,B](seq:Seq[A],f:A=>B,pred:B=>Boolean):Option[(A,B)] = {
seq.view.map(e => (e, f(e))).find{case (_, y) => pred(y)}
}
Here’s some interesting historical background on early “return” statements. It turns out that Dijkstra’s warnings about them have been widely misunderstood.
“Single Exit” meant that a function should only return TO one place: the statement immediately following the call. It did not mean that a function should only return FROM one place. More below:
“Single Entry, Single Exit” was written when most programming was done in assembly language, FORTRAN, or COBOL. It has been widely misinterpreted, because modern languages do not support the practices Dijkstra was warning against.
“Single Entry” meant “do not create alternate entry points for functions”. In assembly language, of course, it is possible to enter a function at any instruction. FORTRAN supported multiple entries to functions with the ENTRY statement:
SUBROUTINE S(X, Y)
R = SQRT(X*X + Y*Y)
C ALTERNATE ENTRY USED WHEN R IS ALREADY KNOWN
ENTRY S2®
…
RETURN
END
C USAGE
CALL S(3,4)
C ALTERNATE USAGE
CALL S2(5)
“Single Exit” meant that a function should only return to one place: the statement immediately following the call. It did not mean that a function should only return from one place. When Structured Programming was written, it was common practice for a function to indicate an error by returning to an alternate location. FORTRAN supported this via “alternate return”: