Pitch: if/else expressions

I personally don’t mind ternary, but I have to agree that if expressions would be more immediately clear (and I really want switch expressions). I have to think when reading ternary code more than I would reading the equivalent if expression. Also, In my experience ternary expressions are often poorly formatted.

If we’re going to do this I think we really should also support switch expressions where each case is a single expression. There is no expression equivalent despite it being very heavily requested and this would be a natural solution.

12 Likes

Since if case let and such aren't currently supported in ternary expressions, I'm guessing they would be here either? It really seems like such a limitation would be confusing to users. If we want this, it should probably go all the way.

6 Likes

I think this should support the full if grammar, but with blocks limited to single expressions. Multiple expressions would still be supported by inline closures where necessary.

Agreed; the limitation preventing pattern bindings in the ternary operator seems purely a matter of syntax, not of any fundamental difference between statements vs. expressions.

Being able to write this would be great:

let result = if let x = x { f(x) } else { defaultValue }

Since the alternative right now is one of these messes:

let result = (x != nil) ? f(x!) : defaultValue

let result: WhateverType
if let x = x {
  result = f(x)
} else {
  result = defaultValue
}

This would reduce the need for some kind of "apply this function only if the argument is non-null" coalescing operator that gets requested from time to time.

5 Likes

Isn't this just map? x.map(f) ?? defaultValue?

11 Likes

I think the best way to express this today is with Optional.map:

let result = x.map { f($0) } ?? defaultValue

EDIT: Expressed even more concisely by @Jon_Shier above.

which avoids the multi-place initialization and the force unwrap. That said, I like the look of the if expressions, and would like to at least explore this direction. It also seems somewhat relevant to bring up the Implicit Returns from Single-Expression Functions pitch because of the single-expression limitation of this pitch. Are there other places where a single expression in a block could allow for a statement to expression conversion?

Also, it's possible today to expression-ize any statement by doing the following:

let result = { if let x = x { return f(x) } else { return defaultValue } }()
// EDIT: Currently, type inference can't handle the above closure,
// so we're actually forced to specify `result: Int`. Is this something
// that is expected to improve?

Are there benefits we would gain aside from a slightly more terse syntax?

3 Likes

:man_facepalming:t2: Ugh, I oversimplified my example and defeated my own argument. I was originally going to post one with multiple bindings/arguments, where map becomes a lot less appealing to use.

1 Like

Just for kicks:

some.long.lvalue[expression]
    = `if`( someCondition, { firstThing() },
        else_if: otherCondition, { secondThing() },
        else: { thirdThing() })

Given...

func `if`<T>(
    _ conditionOne: @autoclosure () -> Bool,
    _ resultOne: () -> T,
    else_if conditionTwo: @autoclosure () -> Bool,
    _ resultTwo: () -> T,
    `else` fallback: () -> T
    ) -> T {
    if conditionOne() {
        return resultOne()
    } else if conditionTwo() {
        return resultTwo()
    } else {
        return fallback()
    }
}

var some = ""
extension String {
    var long: String { get { return "" } set { _ = newValue } }
    var lvalue: [String: Int] { get { return [:] } set { _ = newValue} }
}
let expression = ""
let someCondition = true
func firstThing() -> Int { return 0 }
let otherCondition = true
func secondThing() -> Int { return 0 }
func thirdThing() -> Int { return 0 }

Why not just do switch expressions to begin with?

8 Likes

I have no objection in principle to the idea of switch-expressions, but:

  1. I have not seen many cases where switch expressions would solve a real problem.
  2. If we tie switch expressions to if expressions it might end up killing both of them.

We currently have a useful language mechanism (?:) whose syntax composes in principle, but not in practice. That is a problem. I am proposing to resyntax it so that it does compose in practice. I am not proposing to remove the terser syntax.

We do not currently have an expression analogue for switch the way we do for if/else, so I consider that a separate topic.

Yes please. Please limit the scope to exactly that. simple. elegant. The moment we start mixing if let and switch expressions then no progress will be made.

1 Like

What is the new information that you are bringing to the table? It's not "some people find the ternary operator difficult to read when there are multiple ones nested" is it? Because that is not new information. It's been known to be an issue since long before Swift was born and has probably been cited as a motivation in many of the commonly rejected attempts to get if ... else expressions into Swift.

That said, it is undeniable that your proposed change is much more readable than the ternary version, so I think I would to support it even though I'm not particularly happy about the overloaded keywords.

It also concerns me that you would restrict the contents of the braces to single expressions. Why? Just to make the implementation easier? Normally when you see braces in Swift, you expect to be able to put multiple statements inside. You're planning to break that expectation in order to make the implementation easier. No thanks. Either lift the restriction or find an alternate syntax.

Furthermore, if you keep the restriction and the syntax, you are going to start seeing things like:

some.long.lvalue[expression] 
    = if someCondition  { { doSomething() ; return firstThing() }() }
        else if otherCondition { { doSomethingElse() ; return secondThing() }() }
        else { { doYetAnotherThing() ; return thirdThing() }() }

which is almost the same as the thing you have banned but with uglier syntax and more gotchas.

3 Likes

How do you think this should work? If statements are allowed then we would probably need a new control flow keyword to “return” a value from the conditional. return wouldn’t work because it would return from the containing function, not the conditional. Swift is not going to use an approach of languages like Ruby where the value of the last expression in the block used.

Can you define "real problem"? switch statements in Swift are definitely a pain point for folks who have worked in languages with the expression equivalent. It's often one of the few things folks I've spoken with miss when moving from Kotlin to Swift.

2 Likes

Can you clarify "real problem"? Using a switch in Swift is, to me, always a real problem, because I always return in the cases, and I never mutate in-scope variables. The fact that switch is not an expression is simply a pointless lack of conciseness and expressiveness for people that like to keep their code referentially transparent; in every single switch I write, I'm going to return, in the cases, something that will be assigned to some constant, so a switch-expression would be, to me, much more readable and clear. I also write Kotlin from time to time and its match allows for very concise code, that's also perfectly readable, and it's used like that by the entire Kotlin community, so there you have thousands of cases where a switch-expression has enormous value for thousands of people.

Just to mention, the place where switch is really painful to use in Swift is with closures where you'd like to put a single, concise, readable expression, with inferred type, but because you need a single switch, you have to write multiple returns, that add nothing to the clarity of the closure, and need to specify the return type.

9 Likes

+1 to this as a superb motivating example for switch expressions.

5 Likes

Nope, I can't define it. I've wished for switch-expressions a couple of times in all my Swift programming, and it was nothing I couldn't live without. I haven't seen other peoples' examples. This is just personal anecdotal experience I am reporting to explain why I'm not expanding my pitch.

1 Like

The new information to me is that this issue is not going away. I originally pushed really hard for the tenary to go away but that did not fly.

The conversation exploded into switch expressions and convoluted if expressions and new syntax. I think all of those approaches should be their own proposal. I don’t agree with the is either everything I want or nothing philosophy. I think a very limited useful if expression could have a chance but not a whole new programming paradigm.

2 Likes

For me, the new information is that even advanced programmers have a hard time reading ternary chains as though they aren't nested. Even though a strict BNF grammar for the following would require nesting, no human thinks of if/else chains like this

if a {b} 
else if c {d} 
else {e}

as "nested if statements" unless they are writing a parser, but there are some people who can't map that same way of reading onto:

x = a ? b
    : c ? d 
    : e

Maybe this is old news to you, but for me it's an insight.

'Scuse me, but I haven't "banned," or even proposed to ban, anything. The pitch is entirely additive.

And no, I don't think people will end up writing that, because it's not better or more readable than any of the alternatives. Ternary expressions like this should almost never have side-effects (your doXXX() functions, presumably). The best answer for code like that is probably to break it out into a function with a meaningful name that describes the side-effects. Failing that, I'd still rather read this.

let r: SomeType
if someCondition  { 
    doSomething()
    r = firstThing()
}
else if otherCondition {
    doSomethingElse()
    r = secondThing() 
}
else { 
   doYetAnotherThing()
   r = thirdThing()
}
some.long.lvalue[expression] = r

Finally, if you absolutely must be maximally terse, this is still a better alternative:

some.long.lvalue[expression] 
    = if someCondition  { (doSomething(), firstThing()).1 }
        else if otherCondition { (doSomethingElse(), secondThing()).1 }
        else { (doYetAnotherThing(), thirdThing()).1 }

The ternary operator isn't being misused; it's not being used to its fullest potential. But this is an operator that will not go away, neither in Swift nor in any of the numerous languages that users are sure to encounter. Mastery of ?: is portable to many other languages, just as mastery of many other concepts in Swift is portable.

As pointed out here already, users already have many options to accomplish the same tasks in other ways, and if switch expressions are made possible there will be yet another. What I'm seeing is an argument for better education, not one for yet more syntax. In fact, the latter option will not only do nothing to help users master ?: (which they will still encounter when reading code), but by adding more syntax to the language it will only add to the difficulty of mastering the language for all users and increase the number of choices users have to make to accomplish the same thing, which sets us back instead of forward in terms of making the language accessible to learners.

What we lack as part of the Swift project is a companion cookbook to TSPL that shows best practices, which would be applicable for beginners, intermediate, and advanced users alike--examples of effective use of ternary chaining would definitely be one topic to include (and use of optional chaining and ?? would be others that fall in the same bin).

3 Likes