Pitch: if/else expressions

(Dave Abrahams) #1

First, I realize this is on the commonly rejected changes list, but I think I'm bringing new information to the table.

I would never have been inclined to propose this before, because I like the ternary operator and have no trouble reading it. That said, I keep meeting experienced programmers (really smart people!) that have no trouble reading

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

and yet are confused by the analogous ternary construction:

some.long.lvalue[expression] 
    = someCondition ? firstThing()
       : otherCondition ? secondThing()
       : thirdThing()

Usually when they see this code they ask that I rewrite it in terms of a switch or and if/else that initializes some.long.lvalue[expression] in three different places instead of once, or create some new intermediate variable, either of which is bad for readability, and sometimes, efficiency.

I could just keep telling myself that these people should get over themselves and learn to read ternary chains, but at some point, after hearing the same complaint from many people, one has to accept that user experience is real. The people I'm hearing from are clearly not going to get comfortable with ternary chains. Therefore I propose legalizing this form:

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

Note: I am only proposing that this work when the {}-enclosed clauses are single expressions with a common type. I think this can be expressed in terms of simple textual rewrite rules that transform if/else expressions into ternary expressions inside the compiler.

22 Likes
Omitting Returns in String: Case Study of SE-0255
(Tony Allevato) #2

I've always felt that it was a real shame that Swift stuck so close to its C roots with regard to the ternary operator instead of doing something that was actually legible. I've been programming for a long time and I still hate the ternary operator, so I can imagine how it's even less approachable to newcomers.

I would love to see this happen. switch expressions after that, please?

11 Likes
(Matthew Johnson) #3

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
(Jon Shier) #4

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
(Matthew Johnson) #5

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.

(Tony Allevato) #6

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
(Jon Shier) #7

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

11 Likes
(Frederick Kellison-Linn) #8

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
(Tony Allevato) #9

: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
(Jeremy David Giesbrecht) #10

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 }
(Xiaodi Wu) #11

Why not just do switch expressions to begin with?

8 Likes
(Dave Abrahams) #12

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.

(Chéyo Jiménez) #13

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
(Jeremy Pereira) #14

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
(Matthew Johnson) #15

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.

#16

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
(Elviro Rocca) #17

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
(Matthew Johnson) #18

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

5 Likes
(Dave Abrahams) #19

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
(Chéyo Jiménez) #20

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