Implement return-from
Just another “nice to have” feature: return-from fname to return from a function named fname.
fn f {
fn g { put g; return-from f }
put a
g
put b
}
f # puts a and g but not b
This should only work for lexically enclosed functions, and the function name must be explicit. Otherwise, this is going down a rabbit hole too deep to contemplate.
fn f { g }
fn g { return-from f } # syntax error
fn h { x = h; fn i { return-from $x }; i } # syntax error
If a function calls itself, and then an inner function tries to return from the outer function, it is the immediately enclosing instance that should return:
fn f [&nest=$false]{
fn g { return-from f }
if $nest { f } else { g }
put $nest
}
f $true # puts $true but not $false
What's your use case for this? Honestly it seems very spaghetti-code to me, including explicit lines of jumping in/out of things instead of following the structure of the code.
@zzamboni Escaping from nested function calls is not what I would call spaghetti-code. If this were a facility for jumping in, yes, or another form of goto, I'd agree. But without return-from you might end up using complicated signalling from the inner function (set a flag?) to inform the outer function that we're done, please return. Edited to add: This is exactly analogous to break, continue and ordinary return in these respects.
My examples are of course toy examples, intended to show what this should and (equally importantly) should not do. This facility has no real value until your functions begin having a certain amount of complexity to them: Functions that dig deep into nested data structures and the like, perhaps searching for something specific. When you found what you're looking for, you may want to return that value (with put) and return from the outer function scope. Which can be done with plain return if the inner bits are all anonymous lambdas; but if you have named some of the complicated bits in order to better structure your code, that is where return-from can be handy.
The name and semantics are borrowed from Common Lisp, BTW.
@hanche fair enough - thanks for the explanation!
A more general facility, which avoids the lexicality restriction, would be some sort of throw/catch, using a of tag to identify catch targets ($t in the following example):
fn f [t]{ put f1; throw $t; put f2 }
fn g { catch t { f $t; put g1 }; put g2 }
g # puts f1 and g2 but not f2 or g1
But I don't think I'd recommend it, at least not until the language has matured much more. Still, since it is a related feature, I thought it deserved mention for completeness' sake. Of course, throwing to a catch target originating in a catch that has already exited, would be a runtime error.
Since Elvish is based on Go, which prefers explicit errors rather than propagating exceptions, this seems like a non-starter to me. Too, "This facility has no real value until your functions begin having a certain amount of complexity to them" is also an argument against it. If your function is so large, or complex, that this mechanism is useful it should be refactored IMHO. This mechanism is what I call "spooky action at a distance."
The “spooky action at a distance” argument is why I am not in favour of throw/catch. But I don't agree that return-from falls into this category! At least, no more so than a plain unadorned return inside a lambda inside a loop construct – which is a very common occurrence. You may wish to pull that lambda out to give it a meaningful name, just to make the code more readable. Or perhaps you want to use it twice. But without return-from, you can't because the named function would now capture the return.
Refactoring code is well and good, but it should happen because the code is too complicated or inefficient, not because the language doesn't support a basic escape mechanism.
I can't really comment on the Go philosophy, since I have barely scratched the surface of the language so far. But this is not really about errors or exceptions; it is rather about expected behaviour, such as finding what your code is looking for in a haystack.
We may not come to an agreement on this issue, which is okay with me. In the end, it's @xiaq's decision after all. But I think we have most of the arguments, pro and con, out in the open now
I had a thought while out hiking in the snow today: There is at least a half way decent way to work around the case I alluded to in my previous post, using a flag:
fn foo {
done = $false
fn check-if-done {
# insert big complicated computation
if (some-condition) { put something; done = $true }
}
while $true {
# do some work
check-if-done
if $done { return }
# do some more work
}
}
At least, techniques like this lessen the need for return-from somewhat. (Yes, I am weakening my position with this, but this is not a debate with winners and losers.)
Elvish the language, minus the syntax and pipelines which are obviously very shell-like, is very much in the Lisp tradition. The main influence of Go is the standard library, but almost everywhere else Go is an implementation detail.
Elvish already has exceptions, and return is implemented in terms of exceptions that are caught at fn-defined functions (this is in fact one and the only difference between fn-defined functions and closure literals). This means that if you return from a literal closure within a fn it will actually already do what you want. Nonetheless, generalizing this mechanism sounds like a logical step.
The question I am more concerned about is the way to specify which function to return from; I'd rather we have a general, explicit label syntax that can be used for break and continue too (which can now only escape the innermost loop).
Maybe ^ can be used for this, for example:
fn f {
^f
fn g {
...
return ^f
}
g
}
for f [*] {
^loop
for x [1 2] {
break ^loop
}
}
In the example above the labels ^f and ^loop are applied to the enclosing block (so ^f is for the function f, and ^loop is for outer loop. Maybe it's more intuitive for the label to proceed fn and for respectively:
^f fn f {
fn g {
...
return ^f
}
g
}
^loop for f [*] {
for x [1 2] {
break ^loop
}
}
Also, maybe it's worthwhile after all to make each fn also declare an implicit label that is the same as the function name, closer to @hanche's original proposal.