Pitch: Multi-statement if/switch/do expressions

I haven’t weighed in on this pitch but have watched avidly from the sidelines as various arguments have been put forth and pondered excessively. I know this isn’t a vote, but I’ll offer my opinion.

I’m very excited to see this pitch. I very much hope it arrives in some form.

If it were a perfect world, I’d love to see the result come from the unadorned last line.

If issues with that cannot be surmounted, I’ll be very happy with then or use. return: no, please, never.

I don’t much like the idea of parens: that’s ugly, and I feel the prefix of a leading keyword more clearly marks the result. And converting from a single to multi-line expression block using parens would require tedious changes to both ends of the expression.

1 Like

Since this proposal seems likely to go forward in some way whether the community agrees on a way to do it or not, I would like to contribute what I consider to be the most preferable option for addressing the problem it aims to solve.

This exact problem has been solved before, e.g. in Rust, by implicitly "returning" the last expression in a block. With that in mind, I personally believe the preferable way to solve the problem is to finish the job started by SE-0255. That proposal mentions explicitly the main motivation for not always returning the last expression in a block: at the time, we didn't allow if/switch/do clauses as expressions (see Allow implicit return of the last expression even from bodies which consist of more than a single expression.).

That option is not open to us in Swift because conditionals are statements, not expressions in Swift. Changing Swift into an expression-oriented language would be a radical transformation to the language and is beyond the scope of this change.

Now we have the opposite problem: we do have if (etc) clauses as expressions, and it's weird.

So, instead of trying to bend Swift's syntax by adding more edge cases, I would much rather go back to Rust's existing precedent and enable implicit returns from the last line of any block. That would solve this problem at hand, and make Swift more internally consistent. For example, it has been an annoyance of mine since SE-0255 was introduced that adding e.g. a preceding print into a single-line implicit return causes it to no longer to return the value. Adding the implicit behaviour more generally would solve both issues.

9 Likes

But that perfectly highlights why use isn't great - the do expression isn't using the value 1, it's producing it (or "evaluating" to it, or "resulting" in it, or similar phrasing).

use, to me, throughout this discussion, has always read wrong because I find it mentally more related to inputs than outputs.

3 Likes

I'd still prefer the "((x, y))" form, just to not have an exception to the rule.

Single-statement if expressions are relatively new thing... We could even deprecate the "bare" form of single statement expression as well and always require "dressed up" form (nudging users towards the dressed up form via deprecation warnings initially):

You are right... "produce" then? :wink:

You have to reason it as "use this value in the outer context" and not as in "the inner construct (do, if, etc.) will use the value". This is indeed different from return "returning" a value in a function definition where the caller could be anyone, wheras when "using" a value you can readilly see where it goes. I have no problem seeing "use this piece to fill that hole" as something different from "return this piece to an unknown caller".

That said, my comparison with return was more about how "returning" mirrors the return you see in the code, while "producing" doesn't mirror then at all. I think use/"using" is more coherent than then/"producing" in this regard. But this coherence could also be acheived with produce/"producing" and give/"giving".

1 Like

OK, this ambiguity might be an argument for the more abstract (?) “then”.

Just want to add my +1 to implicitly returning the last expression. Seems like the cleanest way to resolve this and I enjoy using it in other langs.

6 Likes

Four examples that compare implicit vs use, save, or then:

  1. if condition { value }
  2. if condition { use value }
  3. if condition { save value }
  4. if condition { then value }

could be read as:

  1. if true, (then) value
  2. if true, (then) use value
  3. if true, (then) save value
  4. if true, (then) then value

or:

  1. if true, consequence: value
  2. if true, consequence: use value
  3. if true, consequence: save value
  4. if true, consequence: then value

These examples show do:

  1. do { value }
  2. do { use value }
  3. do { save value }
  4. do { then value }

and could be read as:

  1. do value
  2. do use value
  3. do save value
  4. do then value

Addendum regarding save:

We are, of course, saying that the value should be
saved for the assignment – or:

"the variable is assigned the value being saved"

Also: when we save something in an app, we hardly
think of "save" as meaning "with the exception of",
so I don't expect this to be a problem in real life.

2 Likes

Good idea to remind about do. Also else.

if true then value else then value       // ?
do then value                            // ?
if true produce value else produce value // 👍
do produce value                         // 👍
1 Like

Just throwing a +1 behind a keyword that’s in the imperative mood (use, produce, save, etc.), as opposed to then. All the other control flow keywords that result in a non-linear jump in what’s being executed (return, break, continue, throw) are also imperative, and having that “commanding” nature really helps highlight that execution is moving to another location, especially if you’re a beginner or coming back to Swift after a few years off and wondering what the heck that new keyword does.

I would probably vote for use as (at least to me) it implies that one value must be used as the value of the variable in question, whereas produce and save don’t necessarily carry the same connotation of being mandatory.

I’m perfectly fine with implicit last-expression returns being added for those that want it, but I’m a hard -1 on it being the only option - while I don’t think it’s all that confusing, I would personally never use it in my coding style as I like to make control flow jumps very explicit. I would probably even go as far as continuing to use immediately-executed closures if implicit last-expression returns were the only option.

5 Likes

But, there's not much more imperative than then. It's explicitly time-ordering. "This, then that".

use / produce doesn't imply any ordering (beyond whatever might be implied in the general sense that it appears subsequent to other statements). e.g.:

let x = if someBool {
    then 42
    print("True.")
} else {
    7
}
let x = if someBool {
    use 42
    print("True.")
} else {
    7
}

Neither is completely clear that the print call is in an invalid position, but then at least somewhat suggests so. use just says, well, "use 42". For what? Could be for the following statement(s) as much as anything else.

None of this particular matters in the long term, as folks will get used to any of these keyword candidates well enough.

And it does seem like there's a broad opposition to then specifically. I'm just a bit baffled as why, as none of the proffered justifications have been particularly compelling (most are just "I intuit the meaning to be this, which would be mistaken", which is valid as a data point suggesting a challenge for learning, but not super weighty when there's equally if not more logical counter-interpretations).

:man_shrugging:

1 Like

This will and should result in a warning, similarly to how post return keyword is handled. Maybe something like "Code after 'use' will never be executed".

1 Like

Just want to add my +1 to implicitly returning the last expression - the simplest way to resolve this, without using a keyword.

No additional ceremony required. :slight_smile:

8 Likes

Good points.

Here are my thoughts about save:

With save, the {} saves its own value – and then the
value can be delivered to its recipient. The actual delivery
is commanded by the = sign. So, the word save, in itself,
would fully describe the intended action.

"First save one of the items – and then deliver."

Summing up the pluses for the two main contenders ...

Last expression rule:

• less to type in
• no new keyword needed
• consistent in that values vs values with keywords, are not mixed

New keyword:

• courteous and clear
• follows the tradition of writing out return explicitly
• future-proof (doesn't have to be the last expression)

According to the pitch, this is an error, not a warning—then has to be the last statement in the block as there's no reason to have it not be.

Which raises a semi-related question: Does defer work as expected when paired with then? I can't think of any reason that they wouldn't work correctly together, but in this example,

func someComplexFunction() -> Int {
  print("inside someComplexFunction()")
  return 10
}

func anotherFunction(_ cond: Bool) {
  print("entering anotherFunction")
  defer { print("exiting anotherFunction")
  let x = if cond {
    defer { print("after someComplexFunction") }
    print("cond is true")
    then someComplexFunction()
  } else { ... }
}

anotherFunction(true)

I would expect this to print

entering anotherFunction
cond is true
inside someComplexFunction
after someComplexFunction
exiting anotherFunction
1 Like

That's actually not bad:

func foo() -> Int {
    print("start")
    return if condition {
        24
    } else {
        print("hello")
        <- 42
    }
}
Or even this, but one step at a time.
func foo() -> Int {
    print("start")
    -> if condition {
        24
    } else {
        print("hello")
        <- 42
    }
}

@Ben_Cohen I want to raise this question again. What paradigm are you aiming at? From the proposal it seems like you're modelling "one exit point per branch". But what about the imperative interpretation of "then" suggested by @ellie20. It would allow as many exit points as needed, and IMO doesn't shift the paradigm as much as the proposed variant. Is it ruled out already?

I had missed that bit; that seems inconsistent. There's no reason either for a return statement to not be the last statement in a block, but it is legal. It also seems counter to the goal of making it easy to quickly modify a multi-line expression. In a debugging context I might occasionally want to change

func foo(bar: Bar, baz: Baz) -> Quux {
  let x = doSomething(bar)
  let y = doSomethingElse(baz)
  return frobnicate(x, y)
}

to

func foo(bar: Bar, baz: Baz) -> Quux {
  return someSpecificQuux
  let x = doSomething(bar)
  let y = doSomethingElse(baz)
  return frobnicate(x, y)
}

I'd expect the same behavior from then: a warning, but it compiles and produces the earlier value, skipping the later statements. Of course I can comment out the other lines in either situation, but since this is how it already works for return, it's awkward to have a different rule here.

2 Likes

Yes that's the expected behavior https://github.com/apple/swift/blob/a7a0b329f21bf036cf65ebf073af8773b03eb934/test/stmt/then_stmt_exec.swift

2 Likes