Sorry. I'm not quite sure what you mean. Are you referring to the first or second if
?
To the second, highlighted with red circle. The first is not an if expression.
The cost of distinguishing between these two kinds of if
is truly negligible compared to the rest of the compiler (most of the work is early in the relatively low-cost part of the pipeline). And in particular, this extension of it doesn't increase the cost of them.
One of the explicit design goals of SE-0380 was to avoid creating a constraint system comprising the whole of the if
expression. There is no backtracking from the second branch and back to the first. That's why this doesn't compile:
let maybeInt =
if .random() {
1
} else {
// error: 'nil' requires a contextual type
nil
}
You must provide enough type context to avoid needing to backtrack from the nil
back to the 1
to determine that the type is actually Int?
. (let maybeInt: _? =
is enough to tell the first branch it's an Int?
)
This is in contrast to the ternary operator where this compiles just fine:
let maybeInt = .random() ? 1 : nil
but requires that backtracking.1
would be an Int
by default, then the nil
invalidates this and requires re-evaluating the 1
as an Int?
. This can be the source of lengthy compile times, especially when combined with other operators (using ternaries in more constrained environments, like in argument position, is generally much better).
Similarly, one of the (lesser) motivations of this pitch is to discourage the current slight nudge towards long compound expressions in order to be able to stick within the single expression rule for closures, especially since closures themselves end up getting incorporated into bigger constraints, leading to longer compile times and enigmatic diagnostics when they have errors.
If anyone is counting let me add a strong -1.
As many have written the proposal will make reading and reasoning about SwiftUI or other result builder code significantly more difficult.
I am not against a keyword (or unambiguous operator) for a return within a multiline if/switch expression, but it should be explicitly spelt out e.g returnvalue x
or => x
Strictly speaking this doesn't necessarily require backtracking... The compiler could be constructed in a way to treat the first branch as "Int or optional int or something expressible by integer or floating point literal" and then reconcile what it found on both branches to get a more refined answer (be it "ok that's Optional Int" or "no, the two branches are non reconcilable" or "it's way too ambiguous, I need your help determining what you need".)
When I designed the details of Self, the last expression in a method or control structure was always the returned value. But Self was designed to foster creativity, not to prevent bugs. Swift has different goals, but the relative priorities seem to be changing with time. Swift's old design to require the "return" keyword prioritizes clarity at the expense of ease of expression. This proposal reflects a shift in values. one I like, because MY programming is exploratory.
I don't like the effect of adding a new "if" form ("then") on the usability of the language. It burdens both learning and memory when reading code. If you know me, you know that I prioritize simplicity more heavily than having so many choices that you can always find one that does exactly what is needed and no more. Based on my own cognitive abilities, I would rather have to figure out which one of a small number of constructs to adapt, than to have to remember the exact forms of a multitude of constructs. But YMMV.
@David_Ungar2
If you see the previous thread, the initial proposal was with a new keyword (then
), and its title was saying nothing about "last expression". So I wouldn't blame the authors of this Pitch for a shift of values.
What you see in this Pitch is the result of many comments, which did not like the keyword then
. I went quickly through the previous thread (which is quite long), and it is clear that then
, return
, yield
were strongly rejected in most comments, but I'm not sure if there was a clear preference for no keyword at all.
Anyway, after some more effort from the side of authors, this Pitch gives now the opportunity to comment on if/switch/do expressions without keyword, and as a side-effect on the bigger topic of "last expression", which found its way in the new title.
Your "many worlds hypothesis" scheme works with a limited set of possible things to carry forward, but that only works with simple future possibilities like "this might have been optional" or "this might have been an integer or float literal". But consider this example:
let a = [1,2,3]
let x = .random() ? a.lazy.filter { $0 < 1 } : a
The backtracking involved here is very subtle. Initially the value for the first branch is a LazyFilterSequence<[Int]>
. Then in the second branch, the overall type is determined to be [Int]
. So the first branch is re-evaluated. There happens to be a less-favored overload of filter
on LazySequence<[Int]>
that produces an [Int]
â the regular Sequence.filter(_:)->[Element]
. So it then decides to call that instead.
if
expressions veto this kind of thing, by requiring that all the branches can be resolved independently. The only way to make the above compile with an if
expression is to be more explicit:
let y = if .random() { a.lazy.filter { $0 < 1} as [Int] } else { a }
Which is a good thing, because it also makes clear to the author that their attempt to do something lazily was getting thwarted, unlike the ternary example where it undoes the laziness silently.
Multiple people (including me) in this thread have raised the question of how this proposal interacts with Result Builders. If I'm not mistaken, I don't believe these questions have been fully addressed within this thread, however, it looks like they have already been addressed in SE-0380:
The expression is not part of a result builder expression
if
andswitch
statements are already expressions when used in the context of a result builder, via thebuildEither
function. This proposal does not change this feature.The variable declaration form of an
if
will be allowed in result builders.
If I understand this correctly, it's saying that already, today, if an if
or switch
is used in the context of a Result Builder, then the Result Builder will take precedence, so the buildEither
function will be used, and the in-place if
/switch
expression will not be used. (That is as long as the user doesn't explicitly opt-in to if
expression syntax by writing something like:
let foo = if a { "A" } else { "B" }
So it seems whatever ambiguity there could be with Result Builders, is leftover ambiguity that already exists today (since Result Builders can be implicitly applied already, like on body
in the View
protocol). This proposal doesn't seem to increase ambiguity (at least when it comes to code that a human could interpret as either an if expression or a buildEither
).
@localhost shared this example:
var body: some View {
Text("Foo")
Text("Bar")
}
which is currently interpreted as something like:
var body: some View {
Group {
Text("Foo")
Text("Bar")
}
}
// `body` evaluates to something like TupleView<Text, Text>
Presumably, after this proposal, it would be interpreted as this:
var body: some View {
Text("Foo") // â ïž Unused expression.
return Text("Bar") // this return is implicit
}
// `body` evaluates to just Text
The warning would immediately draw attention to the cause of the problem, but this would be a breaking change in many code bases.
It seems worth considering if the "last expression as return value" rule should not apply within Result Builders (just like if
/switch
expressions already don't apply within Result Builders).
(This behavior may already be intended, but I don't think I saw it mentioned explicitly in the proposal.)
The code with Group
is equally confusing.
var body: some View {
Group {
Text("Foo")
Text("Bar")
}
}
Even if the compiler does not inject a return
before the last Text
, to a human reader it looks like the second Text
is the last expression in its closure, so it is the return value. And in case there is some lengthy code between the two Text
, it becomes even more confusing.
The over-reuse of the same keywords and symbols for multiple purposes, which becomes more and more common in Swift, eventually collapses. I think this proposal is at the edge of such a collapse. Maybe the compiler can still deal with it, and maybe in an efficient way, but for human readers it starts becoming ambiguous.
I hope in the future we don't need to compete with a computer (compiler) in solving difficult puzzles. We can resign already, as chess players did years ago.
+1.
It's handy for short use cases with small blocks that just introduce chatter where the addition of intermediate values (for examination whilst debugging) or logging required the refactoring of existing implicitly returned values.
The return keyword isn't going away so keep using it everywhere in your future coding - and of course, your existing code bases - if you don't like the feature.
Bad code can always be written, and newcomers to the language might experience short confusion when coming from other languages - but it would be a terrible attitude to write closures extending more than maybe a quarter/half a page of screen and just ending them with implicit returns with an implied arm cross and "just deal with it" attitude.
A big +1 to this sentiment from me too. I was already planning a similar response while catching up on this thread (before reaching that post).
There are many existing features in the language that are nice to have but would conceivably have had similar arguments made against them if they were pitched today.
Consider $n
closure parameters (e.g. { print($0) }
). "If you have a large closure it's hard to know how many parameters it takes" would be a likely argument to come up in a hypothetical pitch thread. In practice, developers generally only use that feature in non-confusing scenarios (short closures in this case), because most developers tend to naturally limit how confusing their code is. And if not, they can already write exceedingly confusing code in Swift.
I write quite a lot of Rust code and have always found if/switch
expressions easy to skim read from the start. Of course, there are always confusing examples but in practice the return values generally stand out.
I'm completely fine with this concession if it's what gets this proposal through.
As you hint at, bare last expressions for functions and closures are purely a matter of taste, whereas for if/switch
expressions the absence of bare last expressions as a feature (or any of the similar discussed alternatives) is a pretty big gap in Swift's expressivity.
Imo if/switch
expressions have limited use in their current form (despite being very useful when they are able to be used). And either way (with or without function/closure support) this proposal will bring if/switch
expressions from a half-baked feature to the feature that people were actually hoping for when the original if/switch
expressions proposal was in discussion.
Even though I have run into the issue this pitch addresses multiple times, I think the readability concerns far outweigh the benefits here. The motivating issue is just a minor nuisance, IMHO hardly enough justification by itself to introduce such a significant change to Swiftâs syntax.
On the topic of readability, which I think is the biggest concern: a key aspect of what makes code readable for me is the ability to quickly locate all the possible return values of a function, without mentally parsing the entire control flow of the code. Currently this is very easy: either all return values have return
before them or the function is trivial enough that it's obvious by inspection. I think it was reasonable to allow implicit returns where the intent is obvious (such as in one line expressions), but this pitch seems to allow writing large and fairly complex functions where the control flow is not trivial at all yet values are returned implicitly as if it were obvious.
This could be mitigated by the IDE (as discussed above), but code is sometimes read in plain text (or with lightweight syntax highlighting). Plus it feels like a major step back to need to rely on tooling when syntax alone has been able to convey this information clearly until now.
I think this will be particularly confusing when adding function calls to the mix, because the calleeâs signature is not visible at the call site, so it may take a couple looks to realize what is and isnât a return value:
func getProfileImage(id: UUID) async -> Image {
// âŠ
if checkForAccess(id: id) {
let image = if let cachedImage {
cachedImage
} else {
await downloadImage(id: id)
}
switch size {
case .thumbnail:
image.resized(to: .small)
case .detail:
image
}
} else {
onUnauthorized() // <- This returns an Image, but itâs easy to miss.
}
}
Compared to:
func getProfileImage(id: UUID) async -> Image {
// âŠ
if checkForAccess(id: id) {
let image = if let cachedImage {
cachedImage
} else {
await downloadImage(id: id)
}
switch size {
case .thumbnail:
return image.resized(to: .small)
case .detail:
return image
}
} else {
return onUnauthorized()
}
}
Granted, thereâs a familiarity bias here and itâd get easier over time, but even for new developers this would be harder to learn than the explicit keyword. And even for experienced devs, it feels like itâd be much easier for a mistake to go unnoticed.
On the topic of consistency: I feel like this is just âmoving the lineâ of which situations are considered obvious enough for implicit returns to be allowed, not necessarily making the language more consistent. To this point, I was quite surprised by this pitch explicitly mentioning that guard
statements wouldnât allow implicit returns:
Isnât this even more true for functions? It feels odd (and inconsistent) to forbid this:
func divide(by denom: Double) -> Double {
guard denom != 0 else { .nan }
// âŠ
}
For not being clear enough yet allow massive functions with nested control flow statements to return implicitly.
The argument just use a linter if your preferred code style is explicit return
s or just donât use the feature if you donât like it are missing the point. This pitch, if implemented, would change how everyone writes Swift code, everywhere, for better or worse. You can set up your linter to fight the language if you so wish, but everywhere else (Stack Overflow answers, Swift Forums posts, GitHub repositories of open source projects...) you'd need to reason about any code being displayed knowing how implicit return works. At that point, it's always more productive to accept the new status quo of the language and move on than to try self-impose a dialect on the few projects you have the power to do so.
Similarly, just because no language can prevent bad code from being written, I donât think we should disregard concerns about a change making bad code significantly easier to write (or, as in this case, opening the door to a new flavor of bad code that wasnât possible before). It may be worth the trade off, but it shouldnât be left out of the discussion.
Honestly I find it a bit insulting that the counter arguments in this thread are being called "vague hand-waving objections". You probably didn't understand them very well. Apologies for the last sentence, I do realize it was also rude.
The argument "don't use a feature if you don't like it" is valid for an additive feature, which adds something new (with new syntax or keyword), not for a subtractive feature, which removes something already existing and heavily used. The return from a function is a must-have feature, you cannot just ignore it if you don't like its syntax.
And in general it is valid for small things, not for critical things. With some exaggeration, imagine that you go to a hotel and they tell you: there is a bomb in your room which is activated with a code, but if you don't like this feature just don't use it.
But by definition, you're already conditioned to write return at the end of every closure that does so, so just keep doing it. Do you expect anyone to be relying on the compiler reminding them that they've forgotten to return a value when one was expected?
As mentioned by @Andropov above, it is not only how I want to write my code. I write some code from scratch, I copy-paste some other code, and I read a lot more code written by others. So it is not a freedom to use return
, it is an obligation to accept and get used to the absence of return
.
Also, as said above, the first example in this proposal doesn't have a return
in the last branch, and it does not accept a return
or any other keyword as optional.
I think anyone here respects the othersâ opinions, sometimes the formulations should be more diplomatic so to speak. (Some seemed to be offended by my own last comment above, I should have expressed it differently, sorry.)
A hopefully more âdiplomaticâ formulation might be that the author of this comment thinks that one might to easily object a new idea on first impression and that the author rates the âgetting used toâ higher than other people might do. I think this is a valid opinion that does not invalidate the objections formulated by others.
Indeed. "I reject your biases in favor of my own because mine are clearly logically superior because they are mine" is equally "uncompelling". Dismissive rhetoric generally is about squishing the other people into submission to make them go away and "win" that way. (note Ben Cohen has NOT done any of that. He's doing really well. Additionally people who just say it's what they want with enthusiasm, also great! Love the joy.)
Out of curiosity, does Swift/Apple even have a budget line item for Human Factors testing of proposed changes? (I mean real broad recruitment PhD designed Human Factors testing.) I think I brought this up before.
Personal opinions about what will or will not confuse certain groups of users are just opinions. Until they are tested.
I have objections beyond readability but I think a lot of discussions on Swift proposals come down to a bunch of developers having strong opinions about what they THINK will or will not matter to different developer groups. Great. That is useful and not to be dismissed anecdotal data. Thank you for bothering to engage with the community.
I do think it would be nice if the LSG got additional input, actually collected data. Give developers of varying levels of experience actual tasks to complete with different features enabled. Record how much time it takes / what their confidence rating is / etc. Consider it an option 4 to add to the arsenal.
Human factors testing is not the be all end all. It's not always great at creating a new feature. But it can cut through feelings about what will or will not happen when certain significant changes are made. Frequently the results are surprising to all parties.
Recruitment is incredibly expensive, so batch testing proposals would likely have to be done.
I don't mind reading about other people's feeling on a topic. It's informative. But then people... go off the rails. It'd be nice to have "Thanks for your input. We'll for test that" to keep things on the road.