If / else expressions

I generally like control flow as expressions but something that is throwing me as I look at these examples is how jarring it is to see these now that we've been so long without.

I'm not sure, but one way to address this might be with some modification that signals a difference like

$if someCondition { firstThing() }
$else if otherCondition { secondThing() }
$else { thirdThing() }

extensions Foo {
    var localizedName: String { $switch {
       $case .first:  NSLocalizedString(“Foo.first”, nil)
       $case .second: NSLocalizedString(“Foo.second”, nil)
    }}
}

//  (`$` is a placeholder. I don't know what the symbol or modification would be)
// I'm also not sure that each piece needs the `$`

Something like this could make it explicit what is going on for everyone and would make 'the rule' easy to apply to switch and if alike.

Without commenting on the pitch itself, I’m going to push back against the idea of making the syntax different just for the sake of being different.

I understand the urge to make the new thing easily distinguishable from the existing thing, but if we do accept a proposal like this, then in a few years they will both be “the existing thing” and the extra syntax will just be added noise.

For comparison, consider Sharp regret #7 from one of the designers of C#.

7 Likes

My apologies for being unclear. The suggested visual difference is more about calling out the difference in behavior which is what I was getting at in that last sentence. The idea that switch/ifwould sometimes be expressions and sometimes be statements and you would have to parse the whole thing to really be sure is… not good, in my opinion.

Conceptually, adding a visual difference is one thing where this proposal would make if two constructions that look exactly alike.

EDIT: To make my point more concrete, consider @discardableResult. When is the value of a three branch expression discardable?

  • all three subexpressions are discardable
    • Clearly the top expression is discardable.
  • two or one subexpressions are discardable.
    • If we make the top expression discardable, we may allow the author to accidentally throw the expression away
    • if we don't make it discardable, we can give false diagnostics where the author just happened to have single expression branches.

With an explicit difference, you can just make the rule "result is not discardable." because the author made their intent clear.

3 Likes

Why would there be a need for a syntactic differentiator between a switch statement and expression? I would assume a compiler implementation would treat the statement version as returning Void, and that @discardableResult would be implied.

With respect to @discardableResult for expressions, why would this be any different than any other expression? The only time you can specify discard-ability is for functions. Switch and if expressions are not functions.

3 Likes

I initially didn't really like the thought of another form of if-else statement that behaves quite differently. But maybe I could instead think of this as something like the implicit return from single line closures. This might be more teachable if the behaviour of {}-enclosed single expressions is expanded to do “something special but useful” in more contexts (return from a closure/instance method/computed properties, turn if-else statements into expressions [vaguely like returning from the statement], etc). This line of thinking might also suggest other possible uses, e.g. if-statements into optionals.

Swift is often designed by intuition, but this criticism is particularly inapt when applied to this pitch.

I believe Dave is coming to us because his prior intuition that compound ternary expressions can be easily taught did not match real-world usage, and he now believes we ought to do something that should be easier to grasp. His solution is to extend an existing syntax which, real-world usage has shown, doesn’t have this problem.

I suppose you could say that he’s relying on intuition to say that people will understand what an if “statement” in expression context means. But we don’t A/B test Swift’s syntax, so some of this kind of reasoning is necessary in every proposal. There’s little justification to demand more rigor of this proposal.

(Having said that, I’m undecided on the pitch itself. I just don’t think this is a good argument against it.)

8 Likes

I think I might give off the wrong impression here: I’m agreeing with Dave on this point.

I tried applying this idea to some ternary expressions from the standard library. I prefer the if expression version in nearly every case, particularly how it lets me know right up front that I'm dealing with a conditional expression. When assignments use ?: I always have to backtrack once I hit the ? and realize that what I've read so far is only the condition, not what's being assigned.

Simple, common usage from ArrayBody.swift:

// before
_capacityAndFlags
    = newValue ? _capacityAndFlags | 1 : _capacityAndFlags & ~1
// after
_capacityAndFlags = 
    if newValue { _capacityAndFlags | 1 } 
    else { _capacityAndFlags & ~1 }

Return example from Algorithms.swift — this is the only one that potentially increases confusion, since reading it as an English sentence appears to conditionalize the return:

// before
return y < x ? y : x
// after
return if y < x { y } else { x }

From DebuggerSupport.swift:

// before
let bullet = isRoot && (count == 0 || !willExpand) ? ""
    : count == 0    ? "- "
    : maxDepth <= 0 ? "▹ " : "▿ "
// after
let bullet = 
    if isRoot && (count == 0 || !willExpand) { "" }
    else if count == 0 { "- " }
    else if maxDepth <= 0 { "▹ " }
    else { "▿ " }

Inside a function call, from DebuggerSupport.swift:

// before
print(remainder == 1 ? " child)" : " children)", to: &target)
// after
print(if remainder == 1 { " child)" } else { " children)" }, to: &target)

A bigger chunk, from ClosedRange.swift:

// before
let lower =
    limits.lowerBound > self.lowerBound ? limits.lowerBound
        : limits.upperBound < self.lowerBound ? limits.upperBound
        : self.lowerBound
let upper =
    limits.upperBound < self.upperBound ? limits.upperBound
        : limits.lowerBound > self.upperBound ? limits.lowerBound
        : self.upperBound
// after
let lower =
    if limits.lowerBound > self.lowerBound { limits.lowerBound }
    else if limits.upperBound < self.lowerBound { limits.upperBound }
    else { self.lowerBound }
let upper =
    if limits.upperBound < self.upperBound { limits.upperBound }
    else if limits.lowerBound > self.upperBound { limits.lowerBound }
    else { self.upperBound }
12 Likes

My view is that if and switch statements must both be expressions, or neither of them should. To do one but not the other would lead to a frustrating inconsistency. So from that perspective, I see bullet 2. here as a feature.

32 Likes

I strongly agree with Ben. It really doesn't make sense to me for a language to have one of these but not the other.

10 Likes

I also strongly agree.

4 Likes

I find that using parentheses to enclose the sub statements makes it more readable without needing the overhead:

_capacityAndFlags = (newValue) ? (_capacityAndFlags | 1) : (_capacityAndFlags & ~1)

That seems to help with readability. I also question whether if expressions can scale beyond the typical ternary use case. Will many inline statements still be readable? Perhaps with the right formatter, but it seems unlikely a many line statement would be any more readable inline vs. a closure.

1 Like

It'd be great to have switch expressions, but IMO Swift would be better than it is today with limited if/else expressions even if we never get switch expressions. I haven't seen a comprehensive proposal to "expressionize all the things" that has gotten any kind of traction here, and there's strong precedent for rejecting such proposals, so it seems like a very very very long shot. That's probably why you aren't seeing pitches for it. The only reason I pitched what I did is that I think I've discovered—practically speaking—actual brokenness in an existing feature, and that seems like grounds for reconsidering precedent. It could easily be the wedge that eventually gets all the things expressionized, which would be great. Speaking personally, though, unless the core team decides to officially retract its discouragement of the more comprehensive direction, I cannot imagine pitching anything along those lines.

I believe Swift can make progress incrementally even if that leaves us in a (hopefully temporarily) inconsistent state with no certainty that the inconsistency will be resolved. If enough people disagree, or even think what I've proposed represents negative progress, then so be it: we shall not have it. That said, unless those of you demanding full generality do something more (and much more ambitious) than objecting to this pitch I don't see how you are ever going to get what you want.

5 Likes

IMO, I think this should be a prerequisite of this pitch. If the core team decides that Swift got it wrong, and control flow expressions are okay, then we can proceed with adding expressions in a piecemeal fashion. I think it will be hugely annoying and inconsistent at a language level to have only one (limited) form of control flow expressions. And inconsistencies on a language level are far more grave than those at the API level from my perspective.

3 Likes

This is a great idea. Starting with the word “if” makes it clear up front what is happening. I do have two suggestions that I think would improve the proposal:

  1. Require parentheses around the entire if-expression.

This makes it extra clear which type of “if” you are reading (the statement version or the expression version). And it results in more readable code, especially when you are just scanning.

  1. Use the word “then” to separate the condition and the first expression, as suggested by Frederick.

This avoids the need for braces around the expressions, which to me is less confusing, because braces normally enclose statements. And it reduces the amount of punctuation, which is a benefit I think.

Examples:

“Repeat \(n) time” + ( if n == 1 then “” else “s” ) + “.”

some.long.lvalue[expression] 
	= ( if someCondition then firstThing()
	    else if otherCondition then secondThing()
	    else thirdThing() )

These ideas extend nicely to switch expressions.

“Repeat \(n) time” + ( switch n case 1: “” default: “s” ) + “.”

2 Likes

I don't buy this. We currently have several forms of control flow expressions: ?:, &&, and ||. This pitch just adds another form.

3 Likes

I have programmed extensively with ternary operators. I have even implemented them in two separate (shipping) compilers, before working on Swift.

I would be one of those people who would ask to refactor the analogous ternary construction above, or at the very least parenthesize it. Even though I know how it works, it still feels weird after all this time and takes me longer to read and verify the precedence.

+1 to if/else expressions, and hopefully switches too!

10 Likes

While I do agree that the control flow expressions might make code clearer, I'm not in favour of this change.
With the current syntax, when the value a variable takes depends on a condition, I end up choosing between:

  • Using the ternary operator
  • Pre-declaring the variable/constant and assigning to it from a control flow statement

This must be specific to each person, but personally I tend to use the former when it fits on about one line, and I'll use the latter in the other cases (for instance when there are too many else blocks or when the conditions take too much space).
Now if control flow expressions get added, and in particular if/else expressions, when should I use the if/else form, and when should I use the ternary form?
Of course, the if/else form might be easier to read, but then what's the use of the ternary operator? There is the risk that the ternary operator would start feeling like something obsolete. It would be unfortunate for a language as young as Swift to already start having "those things that exist but that don't make sense to use anymore". While this syntax is more modern and easier to understand than ternary operators, their co-existence seem like something that would contribute to make Swift learning curve steeper.

I agree on the fact that if Swift ever gets control flow expressions, having if/else but not switch statements would feel inconsistent.
I can imagine Swift beginners liking if/else expressions, and then having the frustration of not being able to apply the same principle to switch statements (a frustration that would only be higher if they came from a language that supported both). This would be bad language user experience IMO and might even affect more experienced Swift devs.

2 Likes

“Across the board” really means all statements (except maybe decls?) become expressions. You need to generalize do (probably as progn), for (probably as a kind of map), while (somehow), etc. If you consider my pitch unacceptable because it doesn't do more, what principle legitimizes stopping with switch?

The only logic I can find in support of handing just if/else and switch is that it's (relatively) easy to figure out how to specify those features. But what I'm pitching is even simpler and more tractable than that, so it makes a better starting point.

4 Likes

That is not what I meant. I meant "across the board" for conditional statements. I don't mean we should make all statements expressions. Loops expressions in particular seem unwise to bake into the language. The expression form of a loop is reduce which already exists and is as clear as can be.

The principle is that conditional statements deserve special treatment because they require multiple code blocks with subtle relationships (such as case patterns to cases and else if chaining). There isn't a good way to model this in a library as an expression yet there are often cases where code reads much better when a conditional expression is used instead of the alternatives. I don't think the same can be said for other kinds of statements.

It's fine as a starting point but I don't think we should head down this path without committing to switch expressions. The distinction will seem arbitrary and switch will feel crippled relative to if / else.

4 Likes