Big +1 from me
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 theswitch
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.
Yes, but weâre not going to revert that proposal, so this is the language we have now.
The fact that it "doesn't allocate a closure" doesn't change an async function to a sync one, nor does it remove the need to capture any local variables used inside it. By contrast, a block inherently has the same async/sync context and access to the same variables as its containing function.
"That ship has sailed" is a bit different from "no real objection".
And introduce a bunch of new ones, like
At least with one line, you don't even need to explain why you can't use guard
, since there's no way to use guard
and return a value in a single line anyway.
Overall, I think it adds up as more rules, not less.
That is an existing rule.
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 }
this looks very good, but only because each branch is either one or 2 lines
if a branch had many lines instead, it just wouldnât read clearly, but if there was a keyword (like bind
) on the expression returning the value that is bound to goForWalk
it would be much better; also, it would allow for guard
or if
with no else
to be used in the branches⌠it would basically allow for the usual control flow structure we know and love
in terms of raw added complexity, adding a keyword is going to complicate the language less than just ârememberingâ that last expression is bound to the variable: the latter will simply not read well for long branches
That is an existing rule.
As I've said
there's no way to use
guard
and return a value in a single line anyway
So, it's not a "rule", it's just that there's no way to write the code in which this "rule" is "violated". This "rule" is simply a side effect of the "only a single line" rule, and therefore needs no extra explanation.
Background: I have been teaching programing for 13 years now. It is possible that you have been doing the same, and you just don't see the issues that I have. I accept that we can have different experiences when it comes to 'newcomers' and 'new programmers'
Accepting this proposal would simplify/consolidate the existing rules and exceptions, yielding a simpler syntax for newcomers.
That is simply not true. The existing rule comes out of an affordance given to @autoclosure
that felt right because it simplified single line entires that were really easy to debug, albeit sometimes confusing especially when all you were writing was <
. (This is NOT easy for newcomers btw)
Yet, these contexts are so very simple, you would never have pages of code to scroll through to find the return value. You would have exactly ONE line to deal with that that line alone could be the only culprit. This affordance was allowed because it sort of matched what was already happening inside parameters.
It ways always a ay of making closures within parameters kinda behave like the expressions.
You would, for example, never have to say
doThisThing(x: return 1, y: return 2, z: return 3).
Again, these would always be single lines anyway.
So then it was asked that we extend this to all single line closure (I believe).
I kinda objected to this, I think, because it served a sort of philosophical niche in parameters that didn't NEED exist outside of that. I think if Swift was made today, I would have never invented autoclosures as it hides too much of what is actually happening (this is unsafe in my opinion).
But it wasn't a big deal. It was ONE line. There really could be no major confusion about what was being returned.
Single line, allowed. Multi lined, not allowed. Very simple to understand, very simple to keep straight for newcomers.
But not always, because newcomers and new learners of programming often confuse that a body of a function and closure are different than a body in if-statement (this is about to get mudded even more). Why for example, in the body of an if expression can I not simply state the return value and it would early exit? It LOOKS kinda like a closure -- but it's not.
What if it were allowed in if-expressions? Do now all of my @discardableResults become a potential return value?!
That is absolutely dangerous.
What if it were allowed in multi-lined closures/functions and in the course of development I comment out my return statement and now a unsuspecting discardableResult becomes the return value? In the past, the compiler would warn me that I didn't write 'return '. Giving me clear notice that I need to pay attention to what comes out of this function.
What happens if I continue to develop a function, not notice that I had already returned, and I continue to type code.
I have been teaching Swift for 13 years now to young adults. I am honest when I say this, Swift is starting to become a programming language for experts. This is another step in the wrong direction of creating a language that is SAFE, explicit and easy to read.
When I teach programming now, there are things I have found are better to NEVER mention until much later in the year. 1) implicit types 2) trailing closures (all my example code puts the closure inside the (). 3) eliding return in single line closures (with some exception). They very frequently leave the students confused. It is always very difficult to explain why result builders do the funny things they do with if
statements. But each of those are far less dangerous that was is being proposed here. This actually causes the program to yield values in functions that can be passed down through a call chain. In functions that are hundreds of lines long, this is unacceptable.
So please, I beg people to listen to the naysayers who are advocating for something to really be thought about in practice. Not with the brains that we have. We read code all day. We see these discussions and participate fully in the debate of how a new idea will be implemented. Even when we dislike it we will easily understand how it works.
This pitch is a decoration (not a feature) that would only exist because it feels a particular way. Not because it is needed. Not because it is safer. Not because it makes for a measurably better language that enhances the final result (the built program).
For those who feel that this pitch creates consistency despite at the expense of clarity, okay, than I prefer to be inconsistent. Some inconsistencies deserve to remain.
This pitch creates MORE confusion and more rules not less especially when we considered all the other side effects and exceptions that may need to be carved out. And the very fact this is decoration is not needed. We need features for the language that encourage safety not decorations for the language that encourage being less explicit.
I think there would be serious downsides to omitting any version of the then
construct entirely - we probably want users to be able to return early from block expressions, such as in guard
statements, as they do in immediately-executed closures. If the then
construct is indeed necessary, the question becomes whether or not we should permit last-expression returns in addition to the then
construct. It is worth noting that if last-expression returns are permitted, then the then
construct can probably afford a more cumbersome syntax that avoids a new keyword, such as something similar to the Rust syntax: 'label: { break 'label value }
.
I think the claim that permitting last-expression returns results in easier refactoring is overblown. In the general case, adding a side effect or variable binding before an expression would still require cumbersome refactoring: a bare expression would need to be wrapped in a do
block. It is only when the expression in question is already the sole expression of an if
branch, switch
case, or function that the addition of a do
block would become unnecessary; in that case, I think the lesser cost of adding a then
keyword is acceptable.
In contrast, in C, adding a side effect before any expression is lightweight, only requiring the addition of a comma (sideEffect(), value
in C is equivalent to do { sideEffect(); value }
in this proposal). I think the approach taken in this proposal and in Rust is better, because (in addition to allowing for variable bindings) it makes the code easier to read, despite requiring more syntax and more refactoring work. I think an explicit then
construct is better for similar reasons; generally, sacrificing some refactorability for explicitness is a good tradeoff here.
I think the claim that permitting last-expression returns results in easier refactoring is overblown. In the general case, adding a side effect or variable binding before an expression would still require cumbersome refactoring: a bare expression would need to be wrapped in a
do
block. It is only when the expression in question is already the sole expression of anif
branch,switch
case, or function that the addition of ado
block would become unnecessary; in that case, I think the lesser cost of adding athen
keyword is acceptable.
The last-expression rule, as proposed, would also apply to functions. So I don't see how any additional refactoring could be required under this proposal. And without this proposal, the addition of a do
block would only be required if a catch
is needed.
Having said that, I don't oppose the addition of break {label}: expression
, but I don't want it's use to be mandatory.
The last-expression rule, as proposed, would also apply to functions. So I don't see how any additional refactoring could be required under this proposal. And without this proposal, the addition of a
do
block would only be required if acatch
is needed.
What I mean is that, for example,
let x = expression()
would need to be refactored into
let x = do {
sideEffect()
then expression()
}
So in general, adding a side effect or variable binding before an expression requires the addition of a do
keyword and braces. It is only when the expression is directly situated within an if
branch, switch
statement, or function that that level of refactoring would be unnecessary:
func f() -> Int {
// No need to add a `do` block here
sideEffect()
return expression()
}
I think if adding a do
keyword and braces is acceptable in the former case, then adding a then
or return
keyword is acceptable in the latter case.
What if it were allowed in multi-lined closures/functions and in the course of development I comment out my return statement and now a unsuspecting discardableResult becomes the return value?
Discardable results are a strange thing, maybe a warning can be added if it determines the type of a closure (@ben_cohen)? (You then would have to add âreturn â or â_ = â to silence that warning.) Just as with autoclosures, discardable results should maybe only be taught later in an introductory course, at least if there is no such warning. The promise with Swift was never to be easy, but to follow the principle of progressive disclosure.
However, experiences with teaching beginners are valuable as a feedback.
What I mean is that, for example,
let x = expression()
would need to be refactored into
let x = do { sideEffect() then expression() }
I get the sentiment. But this example does not work. The proper refactoring for this would be:
sideEffect()
let x = expression()
You only need to add sideEffect
inside the same block as expression
, if expression
is already inside an expression-like if
or switch
, i.e. it only happens in one possible branch of the code. The other possible case would be if expression
is in a ternary:
let x = condition ? expression() : value
But then to add something that happens before expression
, you need to refactor the whole thing into an if
, because you can neither multi-line nor then
[1] inside a ternary.
At the same time, the ternary example makes for an interesting question: Are expression if
/switch
supposed to be more powerful than a ternary? Is this an intended purpose for them, to have ternary be explicitly weaker? Or was the original purpose to allow long cascaded ternaries like a >= b ? a > b ? 1 : 0 : -1
to be replaced with something more readable?
Because no one would be wondering "Why can't I add a print("oh oh")
to one branch of a ternary". Or, if they did, it would involve a very different design.
I really prefer
break
, leaning towardsbreak = value
at the moment. I think one of the biggest objections to thethen
proposal was the introduction of a new keyword, not the actual usage thereof, but I didn't read the whole thread. âŠď¸
Like a few other people, my reaction to this pitch is âI donât like itâ, with a hard time articulating exactly why. I think my core objection is that when reading code, I like there being some visual marker to say "this is where the return value is; this is what you need to pay attention to". return
keywords explicitly provide that; in a single line closure I think it's obvious from context, as it is for if
or switch
expressions with single line returns; and in a language like Rust it's (subtly) denoted by the lack of a ;
. What's proposed here is using only the position of a line of code amongst potentially many, and to me that just doesn't indicate that something is a return value.
To propose an alternative: I think my preferred solution to that would be to burden the lines that aren't the return value, similar to what Rust does with the semicolon. In Swift, that would mean that if we say everything in a = switch
, = if
, or (maybe) = do
is an expression, you'd explicitly mark the lines which you don't want to return the result for with an _ =
. To pull a couple of examples from this thread:
let width = switch scalar.value {
case 0..<0x80: 1
case 0x80..<0x0800: 2
case 0x0800..<0x1_0000: 3
default:
_ = log("this is unexpected, investigate this") // otherwise you'd get an error that the line is returning 'Void'
4
}
let carBackDoorLabel = switch carBackDoorButtonAction {
case .lock:
"Locked" // returns
print("childSafety: \(childSafety)") // error: wrong return type, unreachable code
"Locked?" // warning: unreachable code
case .unlock:
_ = print("childSafety: \(childSafety)") // prints
if childSafety {
"Locked" // returns
} else {
"Unlocked" // returns
}
print("childSafety: \(childSafety)") // error: wrong return type, unreachable code
}
In other words, the rule would be that in expression contexts, anything that is an r-value (i.e. not a _ =
, let _ =
, or func
declaration) is returned as a result of the expression. To my taste, those _ =
s give enough of a visual indicator that "something is different in this context; the unmarked lines must be expression return values" in a way that the bare last expression just doesn't.
I believe one of the motivations for if
expressions was to discourage the use of ternaries as much as possible. Not merely to improve readability, but to reduce the amount of expensive bidirectional type checking.
For closures, the last expression will be used to infer the type of the outer closure.
The inferred type of
let x = {
// some code without return
return optional?.voidMethod()
}()
depends on the presence or absence of return
('()?'
or '()'
, resp.). What is the type with the last expression rule, if return
is removed?
reduce the amount of expensive bidirectional type checking
Isn't the bidirectional type checking necessary for if
expressions as well?
There seems to be a major need for a new keyword in these sort or sudo-closure as âreturnâ would be ambiguous in control flow statements as expressions.
Yes.
Introducing leak
, an expression marker, used in blocks or switch statements that yield a value.
Since yield
is off the table, the next alternative that comes to my mind is leak
let u: Int = if ... {
...
leak 2
} else if ... {
...
leak 3
} else {
...
leak 5
}
let u: Int = do {
...
leak 2
}
let u: Int = repeat {
...
leak 2 // break with a value
} while true
let u: Int = for i in 0..<32 {
...
leak 17 // break with a value
}
Any statement that follows a leak
triggers a warning or error message.
let u: Int = for i in 0..<32 {
...
leak 17
print (i) // Warning - statement after leak has no effect
}
This is similar to my proposal. In all examples leak
is the last expression, but it can be more general. You can have guard
, inner if
, inner loops, etc., which contain leak
. Essentially you can have leak
in any place where return
could appear in the body if it was a closure.