Pitch: Multi-statement if/switch/do expressions

Strong -1 to this proposal.

I only supported if/switch as expressions because it allowed me to be a bit quicker when I had enums with trivial properties per case, allowing me to remove the return_s. Also it looks a little prettier in Xcode ;)

However before that, I thought that if/switch (didn't think about do) should have allowed single-line implicit returns/returns at all because it's the "returned" expression in that branch of the control flow/scope. I think that this would have enhanced the idea of "implicit returns single-expression(s)" because this would have still allowed stuff like:

// 1
func foo(bar: Bool) -> String {
    if bar {
      "isBar"
    } else {
      // do other stuff, taking up multiple lines
      return "isNotBar"
    }
}

// 2 - assume in `enum Cookie`
var stringValue: String {
  switch (self) {
    case .chocolateChip:
      "Chocolate Chip"
    case .oatmeal:
      "Oatmeal"
    case .sugar(let frostingColor):
      if frostingColor == .blue {
        "Blue Sugar"
      } else {
        // do other stuff, taking up multiple lines
        return "Not Blue Sugar"
      }
  }
}

// 3 - I think that this would have been allowed as well:
// follow the control flow to the returned/resolved value in scope
var myValue = if bar {
  "isBar"
} else {
  // do other stuff, taking up multiple lines
  return "isNotBar"
}

I find this a bit more natural: follow the path of control flow and the first (and only) single-line expression is the expected return/resolving value or we require a return-ed expression for multiple lines within a scope, like we already have. I also find this more consistent since in any context that would require a resolved expression (functions, computed properties, closures, etc.), the rules are the same; just follow the control flow.

if/switch becoming expressions suited my needs desires and I was ... content with it. I still saw it as a small but maintainable anti-pattern and only as a nice-to-have: it allowed me to remove a few return_s in my enums.

However, I find all of the ideas to push this proposal forward to be very anti-Swift-like and only make the language more confusing:

  • Introducing a new keyword
    This only adds another keyword that people have to learn about and is exclusive to this feature. This is why I'm glad that we don't use yield. I echo all resistance against then in this thread and resist all new keywords.

  • First/Last/Any bare expression
    But I also echo all resistance/resist against no keywords. First/Last/Any bare expressions would be - again - exclusive to this feature. It doesn't make sense why this specific feature/context would have different value returning/resolving rules from the rest of the language. So, I would say implementing this new rule would have to be universal*. But while Scala may have this rule and some find it cool, it's very anti-Swift-like and makes reasoning about return values difficult. This "allowance" would change the semantics of the entire language, and I think we should solidify exactly what the syntax for returning/resolving a value looks like in all contexts/scopes**.

  • Using return
    This fits in with my thoughts of how it should work in the first place. Not because if/switch are expressions, but because: it's the returned/resolved expression in that branch of the control flow (see example 3 above).

I would/will never use or allow multi-line/statement if/switch/do expressions in code that I control (even my example 3 above), or even the currently existing single-line expressions if it becomes more than a basic binary if/else. It's an anti-pattern to the safer, easier to read, and more declarative approach of the following below. It requires at most 2 more lines (declaration and traditional newline) and prevents headaches in the future with reading and indentation/linting.

let foo: Int

// if/switch/do your stuff
// foo must be resolved like always in all branches

print(foo)

Counter Proposal

I counter-propose that we change how this entire feature is implemented. Remove if/switch/do resolving as expressions and replace with the control-flow resolving rules:

  • follow control flow/scopes to returned values
  • scopes expecting a returned value with multiple lines must have a return _***
  • scopes expecting a returned value with a single-line-expression will implicitly return that value***

I find this to be more natural, more easily teachable/educational****, and concrete. I also think that this wouldn't break the usage of the existing feature.

Yeah sure this is syntactically equivalent to this feature just with return for multi-line blocks but is at least technically different in description.


tldr: this entire feature should have just been syntactic sugar for an immediately returning closure - we would already have this and any other future issues sorted out.


*or it would at least begin the slippery slope of proposal threads where that is the new rule.
**see above for my reasoning about control flow.

***But what about nested control statements?

This is ugly, but I surely won't stop you from writing it and it doesn't break existing rules or introduce special rules. I leave all of the other cases as an exercise to the reader.

let a: Int = if b {
  if c {
    if d {
      // do other stuff, taking up multiple lines
      return 1
    } else if e {
      2
    } else {
      3
    }
  } else {
    if f {
      4
    } else {
      // do other stuff, taking up multiple lines
      return 5
    }
  }
} else {
    6
}

****We should never have to say: "if you want to return a value in a function, here are X rules. If you want to return a value in this feature/scope, use Y rules. If you want to return a value in this feature/scope, use Z rules. There is no reason why it shouldn't be consistent.

9 Likes