Is this variation of do feasible? Apologies if it's been mentioned before, it's hard to search for individual ideas.
By itself...
let sandwich: Sandwich = do {
let leaves = chooseLeaves()
let meat = chooseMeat()
let cheese = chooseCheese()
Sandwich([leaves, meat, cheese], on: .wholemeal)
}
In if/else expressions...
let goForWalk = if isSunny do {
let temperature = getTemperature()
temperature > 15 && temperature < 40
} else if isOvercast do {
let cloudiness = getCloudiness()
cloudiness < 0.8
} else if isRaining {
false
} else do {
let coinFlip = flipACoin()
coinFlip.isHeads
}
And switches...
let dinner = switch day {
case .monday, .thursday: Curry()
case .tuesday, .wednesday: Chilli()
case .friday: Falafel()
case .saturday: do {
let leaves = chooseLeaves()
let meat = chooseMeat()
let cheese = chooseCheese()
Sandwich([leaves, meat, cheese], on: .wholemeal)
}
case .sunday: Salad()
}
Of these 3 snippets you give: the first ("by itself") is the feature as proposed.
The third ("switches") would also work as proposed – but is unnecessary in this proposal because you could just write
case .saturday:
let leaves = chooseLeaves()
let meat = chooseMeat()
let cheese = chooseCheese()
// last expression in case is value of this case
Sandwich([leaves, meat, cheese], on: .wholemeal)
As I mentioned here, there's another formulation of this proposal where do expressions are the only thing that gets the "last value" treatment. They could then be used with single-expression-body switch expressions "as is" like in your snippet.
The if one is the one that isn't quite so simple:
let goForWalk = if isSunny do {
let temperature = getTemperature()
temperature > 15 && temperature < 40
}
With the "only do can be multi-statement", this would need to be
let goForWalk = if isSunny {
do {
let temperature = getTemperature()
temperature > 15 && temperature < 40
}
} else { ... }
but I expect this could fairly easily be sugared to elide the outer braces and have if predicate do { be a thing.
The example in the proposal is the same as your first example. For do statements, this pitch just turns the characters () into do and moves them to the beginning instead of the end, and enforces a single return.
Your other dos are not necessary; the unexpressed but implied point of this pitch is to replace {} () with nothing, not do.
I.e. today's
let goForWalk = if isSunny { {
let temperature = getTemperature()
return temperature > 15 && temperature < 40
} () } else if isOvercast { {
let cloudiness = getCloudiness()
return cloudiness < 0.8
} () } else if isRaining {
false
} else { {
let coinFlip = flipACoin()
return coinFlip.isHeads
} () }
becomes
let goForWalk = if isSunny {
let temperature = getTemperature()
temperature > 15 && temperature < 40
} else if isOvercast {
let cloudiness = getCloudiness()
cloudiness < 0.8
} else if isRaining {
false
} else {
let coinFlip = flipACoin()
coinFlip.isHeads
}
Sorry, I only included that so it was a complete summary.
I just liked the idea of do as a single keyword that would allow consistent wrapping of expressions that had an implicit return. It's not necessary in the switch, but it makes things consistent. In the if/else, it gets ride of the need for the extra/awkward indentation and braces in your unsugared example.
You cannot use return in its original meaning, since unlike break there's no such thing as a labelled return
You can't use a labelled break with a label outside of the closure. At the top level of the closure, break is a syntax error.
If the closure is inside an async function, and part of the expression requires await, {...}() becomes await (() async -> Type)({...}())
It makes reasoning about isolation, escapability, and copyability of local variables a lot more difficult. You may need to add extra decorations to indicate what is captured and how, when the original intention was just for a block of code to run.
Overall, Swift has a lot of syntactic sugar to hide the difference between a closure and a block of code. However, the difference between the two is far more than just syntax, and trying to sweep it under the rug is an invitation for footguns.
On the subject of do, I actually think it would be good if this worked:
let value: Int = if condition do {
print("branch 1")
1
}
else do {
print("branch 2")
2
}
And this didn't:
let value: Int = if condition {
do {
print("branch 1")
1
} // Error: Cannot convert Void to Int
}
else {
do {
print("branch 2")
2
}
}
By allowing only the "syntax error" to have new behavior, you make source compatibility a non-issue.
Making do blocks into expressions, which this pitch does, literally allows it to do everything the then pitch could do. It just removes the need to write then.
What exactly do think is different here? Both versions require the do branch and each of the catch branches to return the same type, even if the top quote doesn't explicitly mention it.
If we limited the multi-statement branches to do blocks like this, it could also open up another possibility for an (optional) keyword for yielding the value, including guard statements/early exit:
let value: Int = if condition do {
print("branch 1")
done 1
}
else do {
print("branch 2")
guard otherCondition else {
done 2
}
3
}
Introducing new keywords makes the language more difficult to understand (in this context, return is needed, in that context implicit return is available, and in this other context this new keyword is needed – very difficult to explain and remember).
The main use case I see for myself is for if/switch expressions. Adding a log line is a common pain point right now, as is having branches that need just a bit more logic than what would naturally fit onto a single line, so not having to cram everything together to meet today’s expectations is also great.
The fact that functions also get this new behavior is not my main motivation, but it does make the syntax easy to teach and remember: “last expression as return” – no “… well, in these specific contexts”.
I disliked last expression as return value very much in Ruby, but like it in Rust where the type system takes care of the common pitfalls. That fact that Rust has the ; as a signal and Swift will not does not bother me. It’s such a small syntactic signal such that for reading purposes it might as well not be there. The fact that an expression is at the end of a block is the much stronger signal.
This proposal will simplify the syntax, making it easier to teach and remember than today’s syntax, and remove pain points when using if/switch expressions.
Doesn’t it make harder to teach when there are gotchas / exceptions over exceptions? This is like saying pronunciation is easy in the English language thanks to the “rules” and “exceptions” ;).
Isn’t return affected the same way with multiline expressions? Does the proposal mention how this use of labelled break is compatible or not having other kind of sharp edges?
Considering Joe’s answer here ( [Pitch] Last expression as return value - #300 by Joe_Groff ) I am bit confused about how these last two points apply. Also how expression statements and multiline closures do not suffer from the same problem.
do expressions would prohibit returns, just like if and switch. I'll clarify in the proposal.
Consider this example:
func foo() -> Int {
guard x else { return 1 } // ok here
let v = do {
…
guard y else { return 2 } // not ok here
// to fix:
// - either get rid of this guard which might me not easy.
// - or reshuffle the code to not be do expression, which might be painful.
print(e)
e
}
print(v)
v
}
Does the above look ok to you? It does feel a bit inconsistent but maybe it’s just me; I’ll support the pitch even in this form.
BTW, is it guard itself or “guard with return” that’s going to be disallowed in “do” expressions?
The Language Steering Group has talked about this some, but I don't think we've ever taken a strong stance against immediately-executed closures, and it's definitely not an officially accepted position that we should minimize them.
Personally, I think they're sometimes fine and sometimes pretty awkward. In particular, having to make a case block an immediately-executed closure just to stay within the restrictions of the switch expression grammar feels to me like a pretty embarrassing place for the language to end up.
My summary for the last expression as a return value would be:
There are cases that many people seem to be in favor of.
In other cases, many people object to it — as far as I can see because of the lack of clarity or because type inference may not produce the expected result.
I don't think there are any "real" showstoppers, i.e. situations that absolutely speak against it.
As @FlorianPircher noted, simple rules for where the "return" statement can be omitted might be a good thing, although this might not be the perfect solution in some cases.
My own conclusion: Since even today you can't avoid some people writing "bad" code, let's see how this is used and what best practices emerge. In some teams, a linter might enforce certain code styles, as is the case today.
My summary: eliding return is not necessary, it produces no new features, adds nothing of value to the language, solves no show stopping problem and is really just something that a few people think would be nice. It removes clarity, causes some debug friction, puts more guesswork on the compiler all of which is more headache than the “oh shoot how much I hate typing return” headache experienced by the few promoting the pitch.
There is no real objection to eliding return in single line closures/functions.
There seems to be a major need for a new keyword in these sort or pseudo-closure as “return” would be ambiguous in control flow statements as expressions.
Accepting this proposal would simplify/consolidate the existing rules and exceptions, yielding a simpler syntax for newcomers. Currently, you have to say: Last expression as result is possible, but only in this context, that other context (unless you want to also do this additional thing, then you have to use a different syntax altogether), and that context.
if/switch already work without any additional keyword, but with only a single expression. Introduction a new keyword would be in addition to the current situation, making the syntax more complicated.
having to make a case block an immediately-executed closure just to stay within the restrictions of the switch expression grammar feels to me like a pretty embarrassing place for the language to end up.
It is pretty awkward. However, before switch expressions, this would be expressible as:
let value = {
switch someEnum {
case .someCase:
/* complex logic */
return thisCasesResult
}
}()
So this inconsistency is, at least in part, due to the switch expression rule itself, as without it, the closure could alternatively wrap the entire switch statement.