Pitch: Multi-statement if/switch/do expressions

I strongly oppose the use of return. I think it will introduce too much ambiguity when reading code.

I also oppose, although not as strongly, the bare last statement for multi-statement expressions. I think it's just less clear.

Either then or use seem ok if we think this language feature is important enough to warrant a new keyword.

4 Likes

Can you illustrate this ambiguity? Can you explain how it's different than the ambiguity we already have with multiple returns in general, or returns in nested functions or closures?

1 Like

I think returns in nested functions or closures are easier to spot and mean "return from this function". Returning from an expression seems to be harder to visually parse for me reading the examples above.

1 Like

The contention of the original proposal for if/switch expressions is that (editorializing a bit) this existing ambiguity is undesirable and reducing its prevalence would be good. IMO the issues with using return for the keyword discussed in this pitch are the same as the issues in nested functions and closures and that’s exactly why I think it’s a poor choice of keyword.

8 Likes

Yes, I have understood this to be a given throughout all if-expression proposals, and therefore return is not on the list of acceptable alternatives to then. It would perhaps be good for @Ben_Cohen to clarify if this is indeed the case.

1 Like

Personally, I don't see the ambiguity at all here or there, I simply brought them up as a logical extension of believing return in expressions would be ambiguous. The rules around return are actually pretty simple, and a return from an expression is only a slight expansion of the current rule. I found that part of 380 rather poorly supported, as the "extra cognitive load" of returning from a closure isn't reflected by my experience with other Swift developers. Immediately closure syntax is very rare, but when used, devs really had no trouble with it given they'd already used closures and functions anyway.

But assuming this is all the case and most Swift users do see ambiguity here, does this mean there will be further proposals to rectify the issue and require then in all nested contexts, including immediate closures? All of these constructs will still exist, and leaving the existing ambiguity with the additional randomly different keyword then, would actually make the situation worse. It's much like the issue that 335 created: new existential use cases must use any, but the existing cases can keep using the bare protocol name, creating confusion among users when seemingly random features require different syntax for the same thing. (Of course I, having participated in the discussion, know why it's different, but no one who didn't, and even some who did, would know that.) So is this a potential fix in the future? If not, the cure seems worse than the disease here.

(As another aside, if nested returns are ambiguous, aren't nested thens ambiguous as well? If that's true then this proposal has solved only a single layer of a single version of this problem.)

6 Likes

I’m on record as not liking keywords that require such heuristics. I’m also not fond of how then doesn’t mean the same thing it has meant for decades in the phrase if-then. In every language with an explicit then keyword that I’m aware of, the then keyword introduces statements; in this proposal, then terminates those statements.

My proposal for the bikeshed color is to leverage a different pattern already present in Swift: introduce a new identifier $? that is analogous to the existing closure variables $0, $1, etc. Assigning to $? from within a block turns that block (and the expression it is syntactically part of, if any) into an expression. To avoid expanding the scope of the pitch, assignments to $? can be constrained to the last statement in an execution trace through the arm of an if or switch statement:

extension MyEnum {
  var name: String {
    switch myValue {
      case .one:
        $? = "one"
      case .two:
        $? = "two"
    }
  }
}
3 Likes

That makes no sense to me... Using a different mechanism (a keyword or not) is good to reduce potential ambiguity (in this case between expression blocks and closure blocks), while having the same mechanism (a keyword or not) in both expression and closure blocks will increase ambiguity.

Nested then (or use or a bare last statement or whatever we end up with) are as "ambiguous" (if by that you mean that out is not instantly obvious where the control goes to, as among the nesting brackets you have to choose the right one which might take a couple of seconds) as nested breaks from loops and nested returns from closure blocks. Just the pitch tries to solve one particular thing about if/switch/do expressions and not the other potentially (and tangentially) related issues.

"?" has a strong "Optional" connotation, which is not appropriate here. Could that be just "$" ? Or even "value", to follow the tradition of "oldValue" / "newValue" we have in setters. Similarly to having a simple "error" variable in the catch blocks instead of some cryptic $ variable to mean "error".

I think it might be conceptually simpler (as well as simpler from implementation point of view) to not have this artificial restriction. Besides the value could be "readable" instead of "write-only".

let x = if condition {
    value = 0 // or $ = 0
    modify(&value)
    print("current expression value: \(value)")
    value = 42 // final value
    print("final expression value: \(value)")
}

If now the problem is a loose sense of "ambiguity", there really isn't any since there's straightforward rules. With return, it will just return the expression to the closest block that wants to capture it, whether it's a variable definition or function/closure return. This idea is nothing new and fits in neatly with everything control-related in the language. Even return labels have popped up which would fit right in.

If the problem with "ambiguity" is: "Upon first glance, I don't understand where this return is going" then that's a problem with this entire feature imo, not the usage of return. It's already been said that it's quite easy to miss the let foo = syntax followed by a large if/switch block which is why this feature will probably be a code smell to many.

I don't think that special resolution rules to identify whether we're in an if/switch block or an if/switch expression upon scanning helps as much as people think it does. The conversation then becomes:

  • For if/switch statements, use this syntax.
  • But if you want to use a statement like that for an expression, you need to use completely different syntax which may also have completely different return rules from the rest of the language.

That requires far more teaching and mental overhead upon "first glance".

We already have rules for return-ing a value. Anything new added for this specific feature I only see bleeding to the entire language - no matter what the result is.

3 Likes

$? = Expr is the best syntax proposed so far.

It could be refined further to write simply $> Expr.

extension MyEnum {
  var name: String {
    switch myValue {
      case .one:
        $> "one"
      case .two:
        $> "two"
    }
  }
}

$> Expr could be read as a special kind of return Expr.

1 Like

Like I said, I don't see any actual ambiguity here, my point was simply that, if this issue with expressions is so bad it requires a whole new keyword, will we see future proposals to fix the same issue in other contexts? If the answer is no I'd argue that the original issue then isn't serious enough to justify a new keyword. The same argument applies if your solution would be different keywords (or other constructs) for the other nested cases. Given I'm pretty sure the answer is "no, there will be no other proposals", I conclude that the original justification is somewhat overwrought.

3 Likes

Well, I could pitch the idea of using different brackets for function/closure blocks and expression blocks which would reduce "ambiguity" (in the above discussed loose sense of this word) "by half". Although realistically I don't think it could be accepted now. Not making the situation any worse (where "worse" is "making code more complex to read") is what we could do now.

At the end of the day I would say that adding a keyword that is only used in a single narrow case, a case where users would instinctually reach for the existing one, would be the worse outcome.

6 Likes

I am guessing you arrived at $> by analogy to ->. I think any identifier beginning with a $ really ought to behave like a variable, not a statement. But -> might work well enough:

var name: String {
  switch myValue {
    case .one:
      -> "one"
    case .two:
      -> "two"
    }
  }
}

If break were accepted in if statements from Swift 1.0 onwards, I don’t even think we’d need to have this discussion because allowing break to take a value would be the obvious choice. Unfortunately, you can use break within an if to escape any number of containing for, do, or switch statements, so changing it to terminate the enclosing if would be a massive source break.

2 Likes

As I said before, the only time I would reach out for "return" is when returning from closure or function block, and I don't think I am alone. Whether that "proper" type of return is allowed or prohibited in if/switch/do expression is a point of additional discussion (we could discuss it later on outside of this pitch).

Adding a keyword to Swift is bad on my book as well – Swift already has too many! At some point to add a keyword you'd need to remove a keyword (or three) to not increase the overall complexity, as otherwise Swift one day would be too heavy and collapse under its own weight the same way big land reptiles did or PL/I.

I believe there is no need for a keyword (old or new). As someone who constantly toggles between these two forms:

let closure = {
    value
}
let closure = {
    print("debug: \(value)")
    return value
}

potentially a few times until the code finally settles, and hates doing so, I fear the same would be with the discussed feature if a keyword (any keyword) adopted:

let x = if condition {
    value
} else { ... }
let x = if condition {
    print("debug: \(value)")
    use value // then, <-  -> $ etc
} else { ... }

Thus I believe we have a unique opportunity to kill two birds with one stone and fix it everywhere:

let closure = {
    value
}
let closure = {
    print("debug: \(value)")
    value
}
let x = if condition {
    value
} else { ... }
let x = if condition {
    print("debug: \(value)")
    value
} else { ... }

I suspect you're raising that argument Big Bang style, but to be honest I think that would be a welcome direction. Though it might be [the opinion of the Swift team] that changing that syntax is impractical now irrespective of its merits.

I'd forgotten that Pascal syntax. It has an elegance to it, once you get past its foreign appearance [to most people today]. Though it's not @tera's original point, if you imagine that := in that syntax actually does function as return, then this "returning through named assignment" (or whatever the canonical term for that is) is really good for making control flow clear (though less so for brevity). In the same way as named loops are clearer than anonymous ones [when break / continue are involved).

There's an analogy that might be pertinent here, between named functions and anonymous functions (closures, loosely speaking). We sacrifice some explicitness - e.g. an actual name which hopefully explains the function's purpose - for convenience / brevity. if/switch expressions essentially do the same thing.

That's one [valid] way to look at it, sure. But you can also choose to rationalise it a little differently: that the proposed use is in fact inline with tradition since it's following the general pattern "if <this> then <that>". The key distinction is that it's in a declarative form, not imperative, so the key "then" bit is the ultimate value, not the steps involved in creating it.

We don't seem to have a good analogy for this in plain English, though, because it seems unnatural to speak in conditional expressions with this particular grammar - it's quite awkward to phrase it "the comfort level is if the temperature is above 40°C then too hot else it's okay". In contrast, Python's grammar is more natural: "it's too hot if the temperature is above 40°C otherwise it's okay".

1 Like

Also this, which is somewhat awkward:

if <this> { then <that> } else { then <that> }

or, to make it even more confusing:

if <this> { <that> } else { then <whatever> }

I'd reserve then for some future day when we are brave enough to omit braces:

if this then that else whatever

could be literally (as written!) valid syntax one day!
(using bold merely as a poor man's syntax highlighting).

1 Like

That's highly subjective.

Example of the current hard to read code:

func foo() -> Int {
    let x = 42
    iƒ (x == 42) {   // a closure!!!
        print("1")
        return 1     // does NOT return from "foo"
    }
    if x == 42 {
        print("2")
        return 2     // returns from "foo"
    }
}

(I deliberately used a funny "iƒ" function name to make it harder to read, but even if it was, say, "when" or "iff" or "on" sometimes it is easy to miss the fact that the following block is a closure, not a statement block).

Leaving existing stuff and its issues aside (which we can't fix staying within the scope of this pitch), if to further limit ourselves to just these three options:

func option1() -> Int {
    let x = 42
    let y = if x == 42 {
        print("3")
        return 3     // does NOT return from "foo"
    } else {
        print("4")
        return 4    // does NOT return from "foo"
    }
}
func option2() -> Int {
    let x = 42
    let y = if x == 42 {
        print("3")
        return 3     // returns from "foo"
    } else {
        print("4")
        4            // does NOT return from "foo"
    }
}
func option3() -> Int {
    let y = if x == 42 {
        print("3")
        return 3     // 🛑 can't use return here
    } else {
        print("4")
        4            // does NOT return from "foo"
    }
}

option #2 would be a winner to me, followed by option #3, followed by (subjectively the worst) option #1.

Note that I used "a bare last expression" rule here, but equally that could've been "use" or "then" – that's a different (and IMHO a lesser) point.

Terribly sorry to repeat myself. Would try not to.

1 Like

Sorry, but I'm completely uninterested in discussing examples specifically contrived to trick users into misunderstanding their structure. In some codebases this would be considered a homoglyph attack, so I'm entirely unconvinced it's at all useful to this discussion. But returning through expressions is exactly what was initially proposed in SE-380 and was discarded precisely because it is confusing, so it's not relevant to this discussion. Given that decision, we're only discussing how we should return values from expression when implicit returns won't work. In that case option1 is exactly what I'd expect. It's easy to read, easy to learn, and is exactly what users will try when they first get an error about implicit returns not working.

1 Like

Ugly and less immediately understandable than “use” (meaning if you see “use” you might get an idea of what is going on without knowing the language, whereas this syntax makes people rather run away from Swift…).

Sorry for the honest answer, I respect your creativity, but please not.

(I even once proposed the aliases “this” for “$0” and “that” for “$1”, but that’s another story.)