31 Jan 2005 21:05
thoughts on the closure/return/break/continue issue
Just a quick summary on the recent closure / return / break / continue
discussions. I've tried to focus on simple rules which will be easy to
understand from a Java developers view; yet do the kinds of things that
folks using closures/blocks do in other languages which have exploited
the concept of closures/blocks successfully in the past...
I think its a good guideline that return, break, continue (with and
without labels) should work the same in a foo.each {} closure as they
do in a for loop. e.g. Chris's example...
for (i in list) {
if (something) {
break
}
else if (whatnot) {
return i
}
}
could be written as
list.each {|i|
if (something) {
break
}
else if (whatnot) {
return i
}
}
i.e. we should be able to cut and paste the same code from a for()
block to a .each {} closure block and it should work the same. We
shouldn't have to translate the grammar just because we chose to use a
method & closure block rather than using the for/while loop
return rules
============
So return, break, continue should work the same at any point in a
method body - whether it happens to be in a closure block or not.
The basic rule on return is that it is mandatory at the end of any
non-void method body. Clearly there is no equivalent 'return' inside
the closure call as return only means, return from a method call (not a
closure invocation).
For closures, the last expression in the closure is returned to the
invoker of the closure - or null if there is no expression or the
expression has no value.
no dumb expression rule
=======================
We have a no-dumb-expression rule to catch bad expression & typos (like
line terminators in the wrong place)
def foo() {
x = 5
+5 // dumb expression, probably didn't mean a line terminate here
return 2
}
no dumb expression rule in closures
===================================
We allow the last expression inside a closure to be a dumb expression,
but previous ones cannot be.
list.each { def y = it + 1; +5; y } // +5 is a dumb expression
terminating a closure invocation early
======================================
So how do we terminate closure invocations, since return isn't allowed?
Normally in method bodies we can use return to stop (and if necessary
return a value) from some nested loop structure.
Firstly here's an example of how we do it in for loops..
for (i in list) {
if (something) {
continue
}
doSomething()
}
To do a kinda return from a closure invocation, really what we're
doing, if we consider the iteration example above - is to continue on
to the next iteration, stopping execution of the current block.
So we should be able to do the same in .each()
list.each {|i|
if (something) {
continue
}
doSomething()
}
Now when using for(), the block of statements return no value, so a
simple continue is enough.
However if we are invoking a closure and using the value returned, such
as if we are adding up items in a list, then we should be able to
associate an expression with continue.
e.g.
list.sum {|i|
if (something) {
continue 5
}
doSomething()
}
e.g. if we wanted to sum the squared value of all of the even numbers
in a list...
list.sum {|i|
if (i % 2 == 1) {
continue 0
}
i * i
}
Normally the continue grammar is
continue [<label>]
so we have extended this above as
continue [label] | [<expression>]
So 'continue' becomes the kinda 'return from a closure invocation'
statement - and 'return' remains the 'return from method body'
statement.
But there is an ambiguity with the above continue syntax, if we have a
variable and label of the same name...
label = 123
foo.each {
if (something) {
// is this continue-to-label or with-expr
continue label
}
}
y = 44
label: println "Hey"
We could say that you are not allowed to have a label with the same
name as a local variable or field (i.e. that could be confused with an
identifier). So we could say that the above is a compile error, as
'label' is ambiguous. i.e. a continue <identifier> statement can only
have one possible definition of the identifier; if there is a vanilla
name (field, local variable, parameter) with the same name as a label
then its a compile error.
This seems reasonable - as its quite easy to check and enforce and
seems to match the 'do what I mean' rule.
Another option is to make this difference explicit in the grammar via
continue [<label>] | [<continueSeparator> <expression>]
Inside a closure block, we may wish to 'continue to label' or 'continue
with expression' where <label> and <identifier> are ambiguous - so lets
differentiate in them in the grammar. (BTW would we ever want to
continue-to-label with an expression being passed? I don't think so -
can anyone think of a use case?)
So how could we disambiguate between a label and expression. Here are
some examples...
// continue to a label
continue x
// continue with expression
continue | x
continue : x
continue (x)
It might be simpler and easier to read if we could just use an
expression and figure out if its a label or not - and enforce that you
cannot continue to a vanilla name which is ambiguous (there is a label
and local variable, parameter or field with the same name)?
I think this is my preferred solution as its cleaner for the common
case; so
continue <identifier>
the identifier is assumed to be a label and if not, its assumed to be
an expression - but the compiler will generate a compile error/warning
if there is a label and a suitable vanilla name available (parameter,
local variable, field) which could conflict.
The nice thing about this is that the continue statement then works
like the return statement...
return [<expression>]
continue [<expression>]
The main difference being, the case of
continue <identifier>
will look for a label name and use that, otherwise it'll use a
variable/field/parameter expression (and perform the check that its not
ambiguous).
James
-------
http://radio.weblogs.com/0112098/
RSS Feed