Using fallthrough

Hi everyone,

I'm considering no longer covering the fallthrough statement in my programming courses. It's a statement I never use in my own code and I struggle to find examples that aren't artificial or toy examples.

Do any of you have real-life examples where fallthrough is either required, or results in cleaner code compared to other control flow?

Thanks for your input!

I mean, personally, I would just mention it in passing when discussing switch. But I'm no teacher, so I don't know if just mentioning it would entice confusion/poor-use. Generally I rarely use it, and when I am, it's usually in some code that is trying to be a little too cheeky for its own good.

So I would say it's probably safe to leave it out.

Off-topic: I'm also curious, do you cover loop labels in your course? I remember in my introduction to programming course, the professor explicitly didn't cover them (this wasn't Swift, but a few languages support them), and would actually dock points if you used them in your assignments. This annoyed me to no end since they are pretty handy for some problems.

I would be hard-pressed to argue that the standard library is an example of "good swift", since we're willing to jump through many hoops in the name of performance for everyone else, but here's a fall through from the implementation of String. The context here is that other uses of this enum want to handle tagged pointer NSStrings separately, but this one has no need for special handling:

    switch knownOther {
    case .storage:
      return _nativeIsEqual(
        _unsafeUncheckedDowncast(other, to: __StringStorage.self))
    case .shared:
      return _nativeIsEqual(
        _unsafeUncheckedDowncast(other, to: __SharedStringStorage.self))
#if !(arch(i386) || arch(arm))
    case .tagged:
      fallthrough
#endif
    case .cocoa:
        /* implementation for NSStrings here */

Not that I can share. However, it has been invaluable when it is needed. It should at least be mentioned.

Also, you might want to talk a few minutes about it in case your students need to be familiar with other languages where fall through is the default. There is a lot of C, etc., code out there which relies on that convention.

4 Likes

One reason to mention it would be to make it clear the default behavior is different from other languages. If your students ever used switch in one of the many languages where "falling through" is the default, showing them a fallthrough keyword is surely the easiest way to make them realize Swift's behavior is different.

6 Likes

IMO it should generally be avoided, and is generally not very "Swifty". I had forgotten it was even there when it was brought up recently :smile:

I think it should be seen as an advanced feature only to be used in very specific performance and/or legacy-code-compatability reasons. It depends on the audience whether it should be tought, but FWIW I expect that are many people currently getting paid to write Swift that don't even know about it :smile:.

1 Like

Why not? What does it mean to be "Swifty" to you?

1 Like

I've used it when porting old ObjC code into swift. The old code was several if statements working with enum values. It was setup like this:

if (value == Case1) {
    // ...
}

if (value == Case1 || value == Case2) {
    // ..
}

There were several other ifs as well so this was one of the rare cases that I needed to use fallthrough.

1 Like

It's breaks the normal control flow and kind of (IMO) the "Switch" metaphor, makes it so you have to read it very carefully to know the expected behavior, and behaves like nothing else does in Swift.

It feels like the kind of inherently unsafe (in terms of easily produced bugs and unexpected behavior when refactoring) thing that Swift has generally done away with.

I would argue that type of logic is better expressed as multiple ifs rather than a switch, but that might just be me being hyper-critical :innocent:

I have a non-toy example. It seems to be very natural to use while splitting a cubic Bézier curve into two curves at a given point using de Casteljau's algorithm. Or at least it does to me. I’m splitting curves because I have a 20” by 12” laser cutter and want build objects larger the. 20” by 12” and sometimes this is easier to do by just making the larger object splitting the curves, and adding some notches and whatnot down one edge.

As a practical example look at the wooden shelving in the corner of this desk:

func split(atT t: CGFloat) -> (CubicBezierCurve, CubicBezierCurve) {
    // de  Casteljau's algorithm via pomax
    var left: [CGPoint] = []
    var right: [CGPoint] = []
    func step(points: [CGPoint]) {
      guard points.count > 1 else {
        precondition(points.count == 1)
        left.append(points[0])
        right.append(points[0])
        return
      }
      var newPoints: [CGPoint] = []
      let newCount = points.count - 1
      newPoints.reserveCapacity(newCount)
      for i in 0 ..< newCount {
        switch i {
        case 0:
          left.append(points[i])
          if newCount == 1 {
            fallthrough
          }
        case newCount - 1:
          right.append(points[i + 1])
        default:
          break
        }
        newPoints.append(points[i] * (1 - t) + points[i + 1] * t)
      }
      step(points: newPoints)
    }

    step(points: [c₀, c₁, c₂, c₃])
    right.reverse()
    assert(left.count == 4)
    assert(left.count == right.count)
    let leftCurve = CubicBezierCurve(endPoints: LineSegment(p0: left[0], p1: left[3]), control0: left[1], control1: left[2])
    let rightCurve = CubicBezierCurve(endPoints: LineSegment(p0: right[0], p1: right[3]), control0: right[1], control1: right[2])
    return (leftCurve, rightCurve)
  }
1 Like

The exhaustive nature of a switch was far more valuable to me in this case so a switch was much better than multiple ifs.

You're probably not looking for feedback, so feel free to ignore this as it's not intended as criticism. However more generally I think this is a good example where fallthrough isn't necessary, but makes legacy code easier to convert to Swift.

For instance instead of the for loop:

You could instead do:

left.append(points[0]) //points always has at least 2 elements, so this is safe
left.append(points[newCount]) //equal to newcount -1 + 1
(0 ..< newCount).forEach {
    newPoints.append(points[$0] * (1 - t) + points[$0 + 1] * t)
}

I'm not familiar enough with your problem space to quickly grep what the full function does, but I believe the logic is identical. It should even be faster since the switch wouldn't be evaluated every iteration.

Given it looks like this was an existing algorithm that was converted to Swift, I would probably just use fallthrough as well :innocent: but there are very few situations where there aren't other approaches that achieve the same result.

It absolutely was converted from something else...I think pseudocode. There will always be another way to do it, Swift is touring complete after all. The question is "is the other way always better (or at least as good)".

I'm going to have to withdraw this example from "argument for (teaching) fallthrough", as your rewrite is a definite improvement.

In my defense the runtime of this Swift program is measured in seconds (and the runtime if you ignore the box packing to fit the design on an approximation of a minimal number of sheets of wood is really more like measured in "second"), but the actual cutting time is measured in tens of minutes I was optimizing for minimal bugs in minimal time to write. Not so much runtime. :slight_smile:

Maybe I'll have to come up with a reason to implement some sort of p-code interpreter in Swift, I remember taking advantage of C's fall through the last time I did one of those...

1 Like

Can't hurt to rewrite it in Assembly just to make sure that's not your bottleneck :wink:

I think that one elegant use of the fallthrough statement is for running a series of database migrations. Adapted from this StackOverflow answer:

func migrateSchema(from currentVersion: Int) { 
    switch (currentVersion + 1) {
    case 2:
        sqlite3_exec(db, "DELETE FROM types;", NULL, NULL, NULL)
        fallthrough
    case 3:
        sqlite3_exec(db, "ALTER TABLE entries ADD COLUMN touched_at TEXT;", NULL, NULL, NULL)
        fallthrough
    case 4:
        sqlite3_exec(db, "ALTER TABLE categories ADD COLUMN image TEXT;", NULL, NULL, NULL)
        sqlite3_exec(db, "ALTER TABLE categories ADD COLUMN entry_count INTEGER;", NULL, NULL, NULL)
        sqlite3_exec(db, "CREATE INDEX IF NOT EXISTS categories_id_idx ON categories(id);", NULL, NULL, NULL)
        sqlite3_exec(db, "CREATE INDEX IF NOT EXISTS categories_name_id ON categories(name);", NULL, NULL, NULL)
        sqlite3_exec(db, "CREATE INDEX IF NOT EXISTS entries_id_idx ON entries(id);", NULL, NULL, NULL)
    default:
        break
    }
}

The basic idea is to sequentially run each database migration in order, using fallthrough statements to move from one case statement to the next.

7 Likes

This post got me thinking. Fallthrough seemed like the most obvious thing to me when I was first learning Swift, because of the expectation I built from C. However, almost all of the use-cases I would have had for fall through, are better handled by comma delimited cases.

It could have not existed all along, and I wouldn't have noticed, if not for the misleading expectation I picked up from C.

huh...not very robust against someone (probably me) breaking it, but I really like how clean and clear that is!