[Pitch] Valued break

To add a little more fuel to the flame that is the two existing proposals for multi-line switch expressions, I'd like to offer a third option, to see if it's preferred, or stirs up an even bigger flame war.

Issue description

You want to create a value, but need control flow logic in order to do so. i.e.

if condition {
  let value = 3
}
else {
  let value = 4
}
// But we can't use `value` here because
// it's scoped to inside the if blocks

A more common case might be with do-catch when initializing the value:

do {
  let value = try initValue()
}
catch {
  let value = fallback
}

Existing solutions/workarounds

Ternary

let value = condition ? 3 : 4

However, this only works for a single-line option. If you want to do anything extra like

if condition {
  let value = 3
}
else {
  doSomethingExtra()
  let value = 4
}

It no longer translates to a ternary. Also, ternary only works for if, and not for do or switch.

Just use let earlier

Surprisingly enough, this works:

let value
if condition {
  value = 3
}
else {
  value = 4
}
print(value)

Even though let is used, Swift simply makes sure that value is only assigned once.

IIFE

Abusing closures, the following is possible:

let value = {() in
  if condition {
    return 3
  }
  return 4
}()

Bonus points:

  1. We can skip the else
  2. It works with for too:
let value = {() in
  for element in sequence {
    if test(element) {
      return map(element)
    }
  }
  return defaultValue
}()

But, the use of closures breaks other control flow elements:

for element in sequence {
  let value = {() in
    if test1(element) {
      return 1
    }
    if test2(element) {
      continue // Oops! We're not actually "inside" the loop
    }
    return 2
  }()
  print(value)
}

A bigger problem is that return changes its meaning, so you can "return" the value to the assignment, but you can't break out of the outer function inside the IIFE. At least, not without abusing exceptions as well.

Proposed solution

Allowing labelled breaks to be used in an expression and have a value:

let value = mylabel: if condition {
  break mylabel(3)
}
else {
  break mylabel(4)
}

Can also remove the else by using do, as well as adding additional statements:

let value = mylabel: do {
  if condition {
    break mylabel(3)
  }
  doSomethingExtra()
  break mylabel(4)
}

And, like IIFE, it can apply to for as well:

let value = mylabel: do {
  for element in sequence {
    if test(element) {
      break mylabel(map(element))
    }
  }
  break mylabel(defaultValue)
}

And out control flow can still be used using labels:

outer: for element in sequence {
  let value = inner: do {
    if test1(element) {
      break inner(1)
    }
    if test2(element) {
      continue outer
    }
    break inner(2)
  }
  print(value)
}

Note that the label is a must:

let value = if condition {
  // Am I trying to return 3 from the `if`,
  // Or break out of a label called `3:`?
  // This will always be interpreted as the latter,
  // and cause an error if no such label exists
  break 3
}
else {
  break 4
}

While the label's return type can be automatically be determined from the break's value type, you might prefer to define it explicitly:

let value = mylabel(Int): if condition {
  break mylabel(3)
}
else {
  break mylabel(4)
}

Other concerns

Unlike the existing proposals, this proposal:

  1. Does not introduce a new keyword
  2. Does not break source compatibility in any way (break label(value) would currently be a syntax error)

One caveat is that the use of labels inside an expression can conflict with ternaries due to the use of :. To remove ambiguity, ternary takes precedence. If you want to use label with a value inside a ternary, simply wrap it with (), as so:

let value = condition1 ? (mylabel: if condition2 {
  break mylabel(1)
}
else {
  break mylabel(2)
}) : 3

Without the (), mylabel: above would be interpreted as taking the value of a variable called mylabel, and then moving to the else part of the ternary. At which point, the final : 3 would cause a syntax error, and break mylabel(1) would cause an Error, no label named "mylabel" in scope

So, what does everyone think about this? Is it better than the existing proposals? Worse? Comment your thoughts below.

While I still prefer implicit return, this could be useful on its own regardless of the existence of implicit return. I do have one suggestion though, but it might be slightly breaking. My biggest objection to this is that labels would be required to use this instead of merely allowed. If the break needed a colon to turn into a value yielding expression then we wouldn't need a label for this to work.

outer: for element in sequence {
    let value = inner: do {
        if test1(element) {
            // because of the colon, this is unambiguously breaking out of the do block
            break: 1
        }
        if test2(element) {
            // regular breaks & continues work as normal
            continue outer
        }
        // labels can still be used, even to yield a value from an expression
        break inner: 2
    }
    print(value)
}

The colon also has the advantage of having less syntactic noise and less typing. I don't know off the top of my head whether break is a valid label, but if it is this would have to be opt-in prior to the next major version of Swift.

2 Likes

Personally have neutral thoughts about the other proposal, but negative ones for this pitch. This looks like very bad syntactic noise. You acknowledged a solution that should be usedin my opinion:

This shouldn't be surprising that it works. Without type annotation the compiler still knows that the two values can use the same type and are only assigned once.

2 Likes

Unfortunately, I don't think the compiler is smart enough to figure out this one:

let value
outer: do {
  for element in sequence {
    if test(element) {
      value = map(element)
      break outer
    }
  }
  value = defaultValue
}
print(value)

Maybe it should be. But AFAIK, it isn't.

I did try to think of more complex scenarios like

foo?.bar(param: (label: switch ... ))

But this could probably be refactored to

outer: do {
  guard let foo else { break outer }
  let param
  switch ...
  foo.bar(param: param)
}

Which is not significantly worse.

It is smart enough, you firstly have to write code that is valid. Type annotation is required when declaring uninitialized variables. You also don't have a defaultValue variable in your snippet.

A valid snippet would look something like:

let value:String
do {
    var defaultValue = ""
    for element in ["1", "2", "3"] as [String?] {
        if let element {
            defaultValue = element
            break
        }
    }
    value = defaultValue
}
print(value)

Even this is a bit noisy and this pitch will make it even more so, and you should probably break it up into smaller functions.

This is a corrected version of your snippet:

let value:String
outer: do {
    for element in ["1", "2", "3"] {
        if Bool.random() {
            value = element
            break outer
        }
    }
    value = ""
}
print(value)

The code I made abridges definitions above it, so assume defaultValue is some constant, sequence is some iterable (e.g. array) and test and map are defined functions. In which case, your version redefined defaultValue as a variable, and would likely cause a compile error, or at least a warning about the original defaultValue being shadowed and unused.

But, if it's so hard with an abridged version, here's one possible header for it:

enum MyResult {
  case found(withSuffix: String)
  case notFound
}
let defaultValue = MyResult.notFound
func test(_ element: String) -> Bool {
  return element.hasPrefix("This one: ")
}
func map(_ element: String) -> MyResult {
  return MyResult.found(withSuffix: String(element.suffix(from: "This one: ".endIndex)))
}
let sequence = ["One", "Two", "This one: Three", "Four"]

The final value of value in this case should be MyResult.found(withString: "Three"), but there's no way to currently achieve it without a temporary variable or IIFE.

Logically my snippet is valid for that: value is assigned either immediately before breaking from outer, or on the last line of outer, so as soon as value is assigned, it must exit outer, and hence no other assignments to value may execute. But this is difficult control flow to analyze.

Again, that is invalid Swift code (MyResult initializes with a Substring instead of String, and you have to use .endIndex not .length [assuming you have no extensions]).

Please elaborate on how this code:

let prefix:String = "This one: "
let value:MyResult = outer: do {
    for element in sequence {
        if element.hasPrefix(prefix) {
            break outer(.found(withSuffix: String(element[element.index(element.startIndex, offsetBy: prefix.count)...])))
        }
     }
     break outer(defaultValue)
}

// or:
// let value:MyResult = outer: do {
//  for element in sequence {
//    if test(element) {
//        break outer(map(element))
//    }
//  }
//  break outer(.notFound)
// }

Would be better than this:

extension Sequence {
    func first<T>(where predicate: (Element) -> T?) -> T? {
        for element in self {
            if let e:T = predicate(element) {
                return e
            }
        }
        return nil
    }
}

let prefix:String = "This one: "
let value:MyResult = sequence.first(where: {
    return $0.hasPrefix(prefix) ? .found(withSuffix: String($0[$0.index($0.startIndex, offsetBy: prefix.count)...])) : nil
}) ?? .notFound

// or:
// let value:MyResult = sequence.first(where: { test($0) ? map($0) : nil }) ?? .notFound
print(value)

Or how this pitch would objectively benefit the language because I do not see how this does.

Fixed the nitpicks. And, the version with break removes the creation of a single-use extension. As well as leaving open other options for the inner loop, like returning from the containing function without ever assigning to value:

func maybePrint<Element, Value, Seq: Sequence>(
  ifFound: (Element) -> Bool,
  inSequence: Seq,
  then: (Element) -> Value,
  otherwise: Value,
  unless: (Element) -> Bool) -> Void
  where Seq.Element == Element {
  print((outer: do {
    for element in inSequence {
      if ifFound(element) {
        break outer(then(element))
      }
      if unless(element) {
        // This line cannot be achieved with IIFE or an extension method
        return
      }
    }
    break outer(otherwise)
  }))
}

Yes, it's a convoluted example. But I can't model an entire piece of enterprise software inside a single post.

You could probably say that about the linked two pitches. The thing is, there's a sufficient amount of IIFE usage in the wild to have justified making those pitches, despite how much the community is divided about them.

Without saying I support this pitch*, I'll note for purposes of comparison, Rust has this feature, but only for infinite loops (i.e. not any old loop, let alone arbitrary blocks):

When associated with a loop , a break expression may be used to return a value from that loop, via one of the forms break EXPR or break 'label EXPR , where EXPR is an expression whose result is returned from the loop .

(Loop expressions - The Rust Reference)

However, they chose to make their loop labels have distinct syntax, so they don't run into the problem of break foo being ambiguous.

* Even in Rust, nearly every time I start thinking about break-with-value, I decide the function has gotten too big and I can be clearer by either using a higher-order function or defining a new helper function, and then return works fine.

1 Like

It doesn't, quite. It needs to be let value: Int. This is using a Swift feature called definite initialization – it's mentioned in the other pitch, albeit very briefly, because it was more thoroughly covered in SE-380 which introduced single-statement if expressions. I'd suggest reviewing that, as a lot of your exposition here re-hashes that ground.

I don't believe a new syntax that ends up being similarly heavy, generally speaking, to the existing options like immediately-executed closure, is the right next step. The goal is not to find new ways to do things – but to find ways that reduce ceremony, and that reduce the friction of things like going from single-statement if or function bodies to multi-statement ones.

In the case of this idea, that isn't really achieved – when going from this:

let value = switch x {
case .a: 1
case .b: 2
default: 3
}

to this:

let value = switch x {
case .a: 1
case .b: 2
default: print("uh-oh"); 3
}

you would have to go through the chore of adding a bunch of (cluttering) keywords:

let value = mylabel: switch x {
case .a: break mylabel(1)
case .b: break mylabel(2)
default: print("uh-oh"); break mylabel(3)
}

While it solves the problem DI has (you no longer have to state the type) and with closures (return means something different locally), in the matter of new ceremony ways it is worse than the DI and closure options – it has more ceremony, requires you to go back and add the break to each statement, and even requires you come up with a good name for the label (a notoriously exhausting prospect).

As @RandomHashTags points out, some of these more niche control flows are better just expressed as a separate function.

8 Likes

I am aware. The point is to not send people to read 400+ message threads just to be up to date, but rather, sum up the issue in an easily digestible way.

I'll admit that I missed the explicit typing requirement of DI as a potential down side. I wouldn't focus on that specific downside, though, because one could counter-argue that it's not impossible to add type inference to DI (For the same reason there can be type inference for return).

To be perfectly honest, I'm not the hugest fan of SE-380. So I didn't consider "I used SE-380, but now need to change things" as a factor. Rather, I looked at the original use case SE-380 set out to (partially) address.

I will grant that break is as much ceremony as return if you're already inside a function where if/switch is your only statement. I focused more on the case of let value = where return is not an option (not without IIFE, anyway).

Could we make the label optional? So that if there is no label, then it looks more like this:

let value = switch x {
case .a: 1
case .b: 2
default: print("uh-oh"); break 3 // or maybe `break(3)`?
}

That to me accomplishes the goal of reducing ceremony.

[edit: made some errors in the example above, fixed now]

The issue is that you could have

let breakval = 3
breakval: for element in [1, 2, 3] {
  let value = switch element {
    case 2: break 4
    default: print("oh-oh"); break breakval // Did you mean "return the value 3", or "break from the for loop"?
  }
  print(value)
}

It might work if you either force the brackets, e.g. break (breakval), or, as suggested above, use another marker like colon, e.g. break: breakval, break label: breakval.

After thinking about it more, you could achieve lesser ceremony by loosening the requirement of single statement value return to "single statement within its branch". Then this is reduced to

let value = mylabel: switch x {
case .a: 1 // Single statement in the `.a` branch, hence the `break` is implied
case .b: 2
default: print("uh-oh"); break mylabel(3)
}

That way, you only need to change the branch that you're actually changing. break mylabel is no more of an addition than print("oh-oh");, unless you somehow missed the fact that 3 was a returned value (in which case, the addition is a positive change).

Sorry, I didn't see this thread before I chimed in on Last expression as return value, and then again. As with the "then pitch", you're on the right track, but as with guard statements there, you're considering problems that actually don't have the possibility of being problems.

  1. Numbers are not valid labels.
  2. There is no ambiguity because the break has to return the value of the expression, and expressions can't exit outer scope*. This is true even in the probably-never-necessary situation of assigning to a Void variable.
label: do {
  let voidValue = if condition {
    let label = ()
    break label // Would return `()` either way, but has to mean the variable because it's all that's available to an expression.
  } else {
    ()
  }
}

* From today's Swift:

let value: Void = {
  label: do {
    if true { break label } else { } // Compiles
    return if true { () } else { () } // Compiles
    return if true {
      break label // Cannot 'break' in 'if' when used as expression
    } else {
      return // Cannot 'return' in 'if' when used as expression
    }
  }
} ()

As a precedent, there's no problem in current Swift with returning variables with the same names as labels.

The following compiles, but if you try putting

break label

before the return in the if or else statement, you'll get

Missing return in closure expected to return 'Int'.

let value = {
  label: do {
    if condition {
      let label = 3
      return label
    } else {
      return 4
    }
  }
} ()

In current Swift, yes. And from the perspective of source compatibility only, that's fine.

But being able to break out of the outer scope is one of the prime motivations for not just using IIFE. You want to be able to just skip the assignment as a whole, and not use the variable, having an early exit instead.

At a bare minimum, it would mean you'd need to set precedence rules, like "When both are allowed, whichever is defined last counts". Or "An outer label counts over a variable".

Currently, it's never ambiguous. A return's parameter can only be a value, never a label. A break's parameter can (currently) only be a label, not a value. If you allow break to have both, you need a way to disambiguate between the two, precisely because a name collision is not a compiler error.

I see your relevant example now. I don't think it's worth supporting the change from the former to the latter here.

for element in sequence {
  let value: Int
  if test1(element) {
    value = 1
  } else if test2(element) {
    continue
  } else {
    value = 2
  }
  print(value)
}
outer: for element in sequence {
  let value = inner: do {
    if test1(element) {
      break inner(1)
    }
    if test2(element) {
      continue outer
    }
    break inner(2)
  }
  print(value)
}

This is an interesting syntax.

Big +1: The addition of a name (label) next to break, opens the possibility for nested labelled breaks with intuitive syntax (e.g., the outer label may break first, inside the inner expression). This is not so easy without the labels. Maybe you can add an example (in the example with outer: for ..., the outer label is a plain label).

-1: The syntax

break label(value)

doesn't look natural to me. Could it be

break label = value

instead?

Big -1: Having two names for the same thing (the variable/constant name and its label) is an overkill. Is there a way to reuse the variable/constant name as a label?

0 (no point):

Unlike the existing proposals, this proposal:

  1. Does not introduce a new keyword

There is no new keyword, but there is a new syntax for an old keyword. It doesn't make much difference. You have to document a new syntax and I have to learn it. (I don't know why people count keywords and not different syntactic combinations of keywords. In analogy, when I learn English, phrasal verbs are new verbs to me, maybe a little harder to learn if they don't correspond to the same phrasal verb in my language.)

1 Like

Because if you add a new keyword to a language, and a program or library previously using the language happened to use the exact same word (the English language only has so many common words) for a variable or function, you now have source incompatibility.

The same does not happen if you reuse a keyword. Existing code would already avoid using the keyword as a variable or function name [1]. So you only need to make sure that the new syntax using the existing keyword doesn't accidentally overlap possible existing syntax using that same keyword.

That, and every new keyword results in bikeshedding on which exact word should be used.


  1. Often, even if the keyword is contextual, meaning it can be used as a variable name, just because syntax highlighters would be confused by it ↩ī¸Ž

Ok, I see.

This problem could somehow be addressed with versioning attributes, correct? E.g., by adding new keywords or incompatible syntax less often, at major changes. Of course at the cost of compiler complexity and with some inconvenience for programmers.