Proposal: implicit return should return the value of the last expression
Currently return statement in dart is explicit.
@override
Widget build(BuildContext context) {
final hello = 'Hello, world!';
return Center(
child: Text(
hello,
textDirection: TextDirection.ltr,
));
}
If we omit the return statement, the code still compiles
@override
Widget build(BuildContext context) {
final hello = 'Hello, world!';
Center(
child: Text(
hello,
textDirection: TextDirection.ltr,
));
}
but now it returns null. In essence, since all dart methods return something, there already is an implicit return!
Proposal
Instead of returning null, implicit return should return the value of the last expression. So the above code should behave as if
@override
Widget build(BuildContext context) {
final hello = 'Hello, world!';
final ir = Center(
child: Text(
hello,
textDirection: TextDirection.ltr,
));
return ir;
}
Reasoning
Dart already supports implicit returns:
- either by returning null if no explicit return is called
- returning the value of the last expression for "fat arrow" syntax
Implicit null return has a few downsides:
- in case that return keyword is missing (by mistake), the code compiles but results are unexpected
- code formatting does not look nice (e.g. 7 extra chars before "Center" widget above)
Implicitly returning last expression has a few benefits:
- less characters, nicer code formatting
- in case return is missing in the last statement, results is still what was originally intended
- coding mistakes (e.g. last line of Widget fun() being final foo=1+1) would result in compile error
- function return value would be implicitly defined by the last statement
There are languages which implicitly return last statement, like R and ruby. In my experience, once one is used to that paradigm, it is really fantastic. It does not prevent people who prefer explicit return still use it.
It's an interesting idea, which I have already been looking at in other contexts (statements evaluating to a value). There are some syntactic issues which needs fixing, though.
It's not clear what the "last expression" is. It should likely be the value of the most recently evaluated expression statement (a statement of the form <expression> ';'). This includes nested expressions inside other statements.
A void function will likely have some completely unrelated "last expression" the value of which is now being returned.
At least nobody should be looking at that value.
A dynamic returning function may change behavior because it's allowed both return e; and return;/fallthrough-returning-null. If it relied on the latter to return null, it might return a different value now. It's bad style to mix implicit and explicit returns, and we might want to prohibit that.
So, consider:
foo(map, key) {
map.remove(key);
if (test) throw something;
}
Is map.remove(key) the last expression if test is false, but not of test is true (in which cast the last expression is throw).
Effectively it's the same as:
foo(map, key) {
var result;
result = map.remove(key);
if (test) result = throw something;
return result;
}
where the value of each expression statement in the function is stored and returned reaching the end without returning or throwing. (Compilers can optimize by recognizing expressions which are cannot possibly be the last one and only store the value of the rest).
This might affect type inference where we'd currently infer void because a function could fall though to the end, we'd now try to find the least upper bound of all potentially final expressions.
That can work, and for well-written non-dynamic functions it probably won't matter. They already have return statements, and if not, they're likely void functions.
Syntactically, you can't return a map or set literal that way, because a map or set literal cannot be an expression statement.
Map<String, int> foo(String key, int value) {
{ value }
}
The grammar rules for expression statements say that a leading { in statement position is interpreted as a block statement starter. Not a big problem, you can still write return, but an annoying exception.
Should expression statements in finally blocks count:
try {
return foo();
} finally {
counter--;
}
If the expressions in a finally block counts, then this returns counter, not foo(). Most likely they should not. It's an exception to remember. You can still write return ... inside finally (there's a reason C# disallows control flow in finally blocks, it's almost invariably confusing).
If we start making exceptions like that, should expression statements inside loops count?
There is the risk of accidental returns being valid, and hiding that you forgot to write the real return.
int triangle(int n) {
var result = 0;
var counter = 0;
do {
result += counter;
counter++;
} while (counter < n);
// forgot to write `return result;`, a *classical* mistake to make since it's "obvious".
}
This code will be accepted and return the last value of counter++. It has the right type, but because return is implicit, it doesn't stand out that an explicit return was forgotten.
That is perhaps my biggest problem with the idea.
Would we want to be able to break a statement with a value? I think we should always use an explicit return instead, but code like:
int foo(args) {
block: {
compute;
if (something) break block(result);
computemore;
}
if (test) -1;
}
would perhaps want to be able to provide an implicit return value from the block.
That can be achieved by if (something) { result; break block; } too, so not strictly necessary.
It does show that the implicit "setting the result value and continuing" allows a different approach to control flow.
It's one that is already happening today in functions actually having a result variable that they explicitly assign to.
Not sure how readable I think it is when it's implicit.
I'd be more interested in whether we can make statements have values and be usable in expression positions, because then it's explicit that they are in expression position and must have a value, possibly the value of their last expression.
Then you can get "implicit returns" by doing foo(...) => {statements with a value}, but it's not longer completely implicit.
Thanks for consideration and for the feedback. Some thoughts on some, but not all issues:
var glob=0;
foo(){
glob++;
}
this works and returns int, but
var glob=0;
void foo(){
return glob++;
}
does not compile and rightly so. So either the function would need to:
- return int, which might make sense but breaks backwards compatibility
- explicitly return null, which again breaks backwards compatibility
Alternatively, void function could by definition implicitly return null, as today. If one wants to use implicit value return, they would need to simply remove void / add type to the function signature.
foo(map, key) {
map.remove(key);
if (test) throw something;
}
isn't this essentially
foo(Map<int,int>map, int key) {
final r = map.remove(key);
if (1==1) throw Exception();
else return r;
}
dart recognises that return type of foo is int (or dynamic if types are omitted). Throw by definition disrupts the normal flow of the program, so return never even happens in this case.
Map<String, int> foo(String key, int value) => {key:value}; works fine
this topic of implicit returns came up again on twitter and i wonder if a first version of this could simply be
if the last line of a block is a valid expression, act as if it has a return keyword at the front
no more. no less.
i think the edge cases above are interesting but don't necessarily need to be handled. (for the map and set case i would try wrapping them in parens)
if the last line of a block is a valid expression, act as if it has a return keyword at the front
no more. no less.
Heh, that's definitely not what you want:
main() {
var list = [1, 2, 3];
if (true) {
list.remove(2);
}
print('Hi');
}
This program would not print hi. Since the last line of the block inside if is a valid expression, it would implicitly return.
You probably mean if the last line of the outermost block in a function. We could do that but it would literally only save you one keyword in some functions. What about:
int fib(int n) {
if (n <= 1) {
1;
} else {
fib(n - 2) + fib(n - 1);
}
}
It seems like that should work too. At that point, you're basically just making every statement usable as an expression and then adding implicit returns to the language. Which, if we're going to do anything, we may as well go all the way.
I don't like this idea. Explicit return is much clearer, why ommiting return does not carry as much simplicity which justifies decreasing clarity
If loop is last and no return type specified what is returned?
foo<T>(List<T> list) {
for (T item in list) {
returnArgumentFn(item);
}
}
void main() {
print(foo<int>(<int>[1, 2, 3])); // New list or last iteration expression?
}
If loop is last and no return type specified what is returned?
For this to work, all loops would have to be expressions. Probably, it would have the null value to be returned directly from void functions. For functions with non-top non-nullable return types, it wouldn't be allowed to be used as the last expression unless an explicit return was also provided.
Returning the value of the last expression as well as always returning null is not a good idea, especially when the return type is nullable.
A nice trade-off would be to only have implicit returns if there is only a single expression. Swift supports implicit single expression returns and it's very useful and reduces the clutter (see the Swift proposal here)
So instead of
int getNumber() {
return 42;
}
you would write
int getNumber() {
42;
}
but you would get a compilation error for
int getNumber() {
print("Hello");
42;
}
A nice trade-off would be to only have implicit returns if there is only a single expression.
In Dart, that's:
int get number => 42;