I’m a big +1 on this general direction. I like this proposal a lot as a first step. I don’t think it’s sufficient as a destination for this design direction (and realize the authors may not necessarily be presenting it as such). I’m skeptical of whether it’s sufficient as a stepping stone.
There’s some skepticism about this whole feature direction. My experience points in favor of it. The review prompts ask:
If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?
I’ve used conditionals as expressions extensively in Ruby, Elm, Scheme, SML, and Python. The middle three are of course functional languages, where there is naturally no other kind of conditional. Ruby was the first time I’d encountered if-as-expression in an imperative language, and it was bizarre to me at first. (Some reactions in this review thread could have been me circa 2006.) That feeling passed quickly! I’ve come to appreciate the feature a great deal, and miss it in Swift.
My concerns about this proposal all stem from the ways in which it stops short of full generality. In particular, I’m concerned about:
- Lack of multi-statement support
- Lack of support for use in arbitrary expressions
- Lack of the same type inference the ternary expression provides
Taken together, I’m concerned about whether this proposal leaves the language in a good intermediate state even if we do plan on future build-out in this space. Some specific thoughts:
Lack of multi-statement support
(Detailed analysis hidden because it's long; key point: Is there *anywhere* else in Swift where a single statement enclosed in braces cannot transform into multiple statements?)
Consider the vexation of someone who has the following perfectly reasonable code:
let message = if let thinger = fetchThinger() {
"Found \(thinger.name)"
} else {
"ERROR: no thinger available"
}
…and wants to log the error. They have to do one of the two following awkward (and I think non-obvious) things:
// Option 1
let message: String
if let thinger = fetchThinger() {
message = "Found \(thinger.name)"
} else {
log("Thinger fetch failed")
message = "ERROR: no thinger available"
}
// Option 2
let message = if let thinger = fetchThinger() {
"Found \(thinger.name)"
} else {
{
log("Thinger fetch failed")
return "ERROR: no thinger available"
}()
}
Is there anywhere in Swift where a single statement enclosed in braces cannot transform into multiple statements? I don't think so…?
The precedent we've set throughout the language is that any single statement within braces can become many statements, with the addition of a keyword iff the single statement was an expression whose result matters. This holds for closures, properties, and functions. It should hold here too.
It's clear to me that return
is the wrong keyword for if-/case-expressions, but for the example above, by the language's own precedents, a solution with the following general shape ought to be possible (using result
as a placeholder keyword):
let message = if let thinger = fetchThinger() {
"Found \(thinger.name)"
} else {
log("Thinger fetch failed")
result "ERROR: no thinger available"
}
I'm concerned that this proposal as it stands makes a false promise to language users: what looks like a flexible approach is in fact an underpowered ternary, a dead end that breaks precedent and ends up requiring syntactic backtracking that can't help but feel just incredibly frustrating to a language user.
Maybe this is a good enough stepping stone…but part of me thinks it would be better to just steer people to Option 1 above in the first place until if
expressions can pull their weight.
Lack of support for use in arbitrary expressions
(Detailed analysis hidden because it's long)
If I see this code:
let message = "Fetching thinger..."
displayStatus(message)
…my instinct is to consider refactoring away the intermediate variable:
displayStatus("Fetching thinger...")
For this code, shouldn't the same principle apply?
let message = if let thinger = fetchThinger() {
"Found \(thinger.name)"
} else {
"ERROR: no thinger available"
}
displayStatus(message)
We ought to be able to transform it in the same way, but the proposal disallows it:
displayStatus(
if let thinger = fetchThinger() {
"Found \(thinger.name)"
} else {
"ERROR: no thinger available"
}
)
Again, the language has made a false promise. Normal, basic syntactic rules mysteriously don't apply. ** developer frustration intensifies **
I'm sympathetic to the proposal's concern over extreme cases here. That concern is well-founded.
I proposed allowing if
/switch
expressions in arbitrary expression position when immediately enclosed in parentheses (further examples here). Could we make that our first stepping stone? It seems safe enough, and does not present the same mysterious barrier as the current proposal.
Future proposals might be able to allow dropping those parens in more situations, for example as @beccadax suggested here:
(…but then presumably still allow them if enclosed in parens.)
Requiring more parentheses now and maybe making some elidable in the future seems far preferable to making common usage patterns illegal now because we might be able to make them parens-optional in the future.
Lack of the same type inference the ternary expression provides
I agree with Becca's remarks on type inference in her post above.
I'm always sympathetic to attempts to mitigate the horrors of multi-directional type unification, but I simply cannot get my head into a place where I'm comfortable with single-statement if
expressions with a boolean condition (i.e. not if let
) being anything other than completely equivalent to ternary expressions.
Could multi-statement if
branches, and perhaps switch
vs if
, be the bridge where type inference becomes less robust as proposed here?
Again, I like where this proposal is going. And I am tempted to vote +1 as it stands: it hits the most common cases, and doesn't propose anything I'd imagine we'd have to retract in a source-breaking way. (Edit: That last part might not be true.) However, the way it stops short of generality makes me uncomfortable; I suspect its lack of generality is going to cause a lot of frustration, and make developers hate the whole idea when they would have appreciated it in a more fully built-out form. Opinions tend to calcify around those kinds of strong initial reactions.