The problem is that you're not doing anything in the do block, you're just defining a value. do (to me) is an imperative structure. It's literally saying "do this stuff". You don't "do" literals and expressions, so much as you evaluate them or return them.
The inverse, of wrapping the print in do instead, makes much more sense to me as a result. Though it is a little bit magical that you can insert that imperative block into an expression in a way that's purely for side-effects rather than being intrinsically part of the expression. But pragmatically I can see the utility of it.
If it helps, I find it useful to read this as "…else do these tangential things and then resolve to this value".
I think considering "do" an imperative structure is a valid way to look at it- that is why I suggested the second idea, but this proposal is about changing "do" to an expression. In that case it would no longer be imperative.
You are trivially just returning a value in the example, but in a more complex situation you might actually be doing work inside the expression. No different then a function like this:
func foo() -> Bool { true }
I'm having less trouble reading it, but it breaks the flow of the expression for me. "else if" works for me because there is a "}" before the "else". This shifts another keyword to the right. I know there are other cases in Swift that breaks this flow (such as guard statements with long conditionals that shift "else" to the next line)... I'm just not a fan of it... I would be tempted to shift "if" in this example to the next line.
I suppose. But that just makes it worse, IMO, because fundamentally the goal is to evaluate to a concrete value, irrespective of any side-effects. So do is still the wrong verb; it's focusing on the side-effects rather than the primary purpose.
Also, to actually show the more elaborate (and horrible) example case:
let description =
if x % 2 == 0 {
print("x is even")
do { "even" }
} else {
print("Huh, that's odd")
do {
if x == 0 {
print("x is zero")
do { "zero" }
} else {
print("x is non-zero")
do { "non-zero" }
}
}
}
To me that's quite a mess. It's bad enough with all the prints splattered around, but putting the actual expression elements inside do statements is extra confusing.
In contrast, flipping it to put the imperative side-effects inside do:
let description =
if x % 2 == 0 {
do { print("x is even") }
"even"
} else {
do { print("Huh, that's odd") }
if x == 0 {
do { print("x is zero") }
"zero"
} else {
do { print("x is non-zero") }
"non-zero"
}
}
…doesn't solve the overall issue of noise, but does:
Require fewer levels of indentation.
Make diffs much cleaner because the expression lines aren't touched when adding and removing the debug prints.
Mean that if you can mentally filter out the do blocks while you're reading this, you can read the unadulterated expression.
I should have linked back to my earlier example. I intended that any expression statement could go in the trailing position. So the "do" with an "if" in it would be unnecessary. I think most of the time we wouldn't have so many breaks out of the expression, but that would be good to check against real code.
This might also be complementary with the proposed "then" keyword or use of semicolons, so that could be another way to reduce the noise.
let description =
if x % 2 == 0 {
print("x is even")
do { "even" }
} else {
print("Huh, that's odd")
if x == 0 {
print("x is zero")
do { "zero" }
} else {
print("x is non-zero")
do { "non-zero" }
}
}
I do like that it is easy to filter out "do" blocks in your example if they were left in the imperative world. It just feels like people really want do-expressions for returning from areas where exceptions are handled.
Fair enough. I think what you're saying is you don't like the adverb; you'd be more comfortable with a verb in the sense of return. Which I can understand, even if I'm not (pragmatically) as bothered by then.
Through this I can see the appeal of implicit last line returns, as a way of shortcutting the naming problem by eliminating the keyword entirely. And if I were coming into this academically I'd probably be all for it. But as mentioned [way] earlier in this topic thread, real-world experience with result builders has really burned me, and I really can't stand magical implicit returns / yields / whatever anymore.
Though I am perfectly comfortable with implicit returns for single-statement blocks, and use it zealously because I love the cleanliness of not having return keywords everywhere. I think the key difference is that with them there's no possible ambiguity. There's literally one 'statement' (often just a variable name or a literal, or an expression) which is used in its entirety to define the result. Whereas last-line-implicit-returns (or result builder's "any random line might implicitly yield, good luck") is frustratingly ambiguous and potentially misleading. And I'm not alone - the Swift compiler clearly agrees with me given that, despite herculean effort by the compiler team, it still can't remotely-reliably diagnose type or syntax errors inside result builder expressions.
If I've understood correctly, the purpose of making do { } evaluate as an expression is just to replace the immediately-executed closure syntax? The two are entirely identical in all respects, but the advantage of do { } is simply the following?:
Why would these same users not be similarly confused into thinking that single expressions must be encased in do { }?
Ok, but that seems like too much syntax that can't be collapsed. It isn't exactly the same as my example since it is now a single-expression. It also doesn't allow a guard that returns from the function which is one of the issues this pitch aims to solve. It feels like it is abusing the immediately executed closure syntax which is primarily for initialization.
I’ve been arguing that if we introduce the behavior that do { } does not disrupt the declarative syntax then the frequency of genuinely multi-statement value calculation will be rather low, and therefore this syntax will be appropriately heavy.
guard can of course be used inside of the closure - it’s true that in the middle of calculating a value that is declaratively described using an if/switch expression one would not be able to return from the enclosing function, but are we sure that that’s something that’s important to enable? The only use of guard in the original post is not that, and would still be possible inside of an immediately-executed closure:
I’m confused where this interpretation originates: nothing in the pitched feature would allow for guard to be used as an expression, and indeed it states that this is not something we’re attempting to do.
Ok. Good clarification. The pitch as written doesn't prevent guard, so it would be allowed before "then". It was brought up in the prior if/switch-expressions proposal. "guard" and logging were specifically brought up by core team members as areas they were deferring to future pitches. I don't remember if it was in future directions or just in the discussion.
However, I will defer to those pitching this. Maybe that isn't a concern to allow that one day anymore. Ultimately these features affect coding style and if it isn't seen as needed in real-world code, then it wouldn't be a priority. I guess if guard doesn't work in an expression with a proposed syntax, maybe it is still a valid idea to be surfaced.
I think this pitch and the prior if/switch proposal is following a very Rust-like view. Supporting guard makes sense in that context.
I think spelling this keyword “break” instead of “then” would make this pitch more agreeable because it will slot into people’s existing understanding of control flow. Hopefully it’ll feel less foreign.
Bare style should be reserved for singular statements, as it is with functions. If we want to expand bare style implicit returns, that should be a separate proposal so it can be handled uniformly.
I don't think "break" can be used unless the syntax changes since it would be ambiguous with breaking to a label. Maybe break { "A result value" }
I agree that some limited form of bare-style implicit results could be complementary to "then" or "break" and possibly go in to some future proposal. Adopting "then" or "break" doesn't completely shut the door on implicit results. Just like you can have both implicit and explicit returns that are favored in different contexts. It is obvious that unrestricted bare-style implicit results would never work for everything due to the ambiguity, so a keyword to specify the result makes sense even if it ends up being loosened to allow implicit return on non-ambiguous statements later.
I think for any fans of implicit results, adopting this pitch as written doesn't mean it is end-of-the-road for implicit results and the perceived discontinuity in the language. It just means implicit and explicit results might each have their own place.
I also think that the semicolon option (discussed in the pitch as an alternative) should be taken seriously. There isn't much in this thread discussing that. I think if we expect most of these cases that break away from the flow of the expression are very short (no more then a few statements) then I think semicolons might be a good way to indicate they are still part of the expression. If the goal is to be like Rust and do lots of imperative work mixed together in the expression then semicolons would be a bad idea because we are then just adding semicolons everywhere. Rust often treats these expressions as mini-functions where they just want to store the result in a variable at the end. Notably, long do-expressions probably wouldn't look good with semicolons since they would generally be doing a lot of imperative work.
I don't really have an answer for this. I think I would be fine with either approach.