On chains, nils, and quiet failures

There's a ton of optional chains in my app (foo?.bar?.baz?.qux()) and they are a great tool to keep the source concise, however some of those ? might fail silently and the failures might go completely unnoticed. In addition to auditing all those hundreds (thousands?) of call sites to distill and modify the ones that are harmful from the ones that are benign (and, equally important, keeping auditing them constantly in each and every change) I am looking for something in between ? and !, that will work as ? ("the app continues") but also work as ! (in that I am being told about those instances by seeing their occurrences in the console).

I was thinking abut creating my own postfix operator, e.g. ^ that would combine the best of two: it would optionally log those occurrences along with time and source location, but this operator looks impossible to implement in Swift. The best thing I cooked so far is either an awkward looking caterpillar foo[]?.bar[]?.baz[]?.qux() or the one that looks ok: foo^.bar^.baz^.qux(), but doesn't record file/line of the caller, doesn't allow qux() to be mutating, and similar to ! it traps on nils without ability to continue.

Would there be an appetite for having a compiler switch + an environment variable or a similar opt-in runtime mechanism to visualise those nils happening under the hood?

3 Likes

My take is: don't do that at all (except in rare circumstances).

  • In Obj-C days, we did this all the time (with a null pointer standing in for a Swift optional). We'd write a slab of code ignoring nulls, and check at the end whether we got a non-null result. This worked well in Obj-C, provided you'd learned to avoid the pitfalls, because ignoring nulls was demonstrably harmless in many cases where an eventual null or non-null result was all you needed.

  • I don't recommend "carrying" optionals like this through Swift code, though, because Swift Optional is a semantic thing, not just a side-effect of the dereferencing mechanism. Swift code seems easier to read and understand if you handle nil-valued optionals when they first appear (using guard and if, for example).

  • In other words, the beauty of Swift optionals is that they allow you to elegantly eliminate optionals and extensive use of ? from most of your code. You don't need to reason about whether propagating them is safe or sound. Something like:

    guard let baz = foo?.bar?.baz else { … } // It's clear why/when we're not quxing here
    baz.qux() // We know here that we're really quxing that baz!
  • So, I'd suggest that a new postfix operator would have the wrong effect on code writing, by encouraging the carrying of optionals instead of their elimination.

The cases where you might genuinely want to carry an optional value forward are when you're carrying or computing a value to be stored in a property which is itself optional (but often not even then).

7 Likes

My experience with optionals is that they’re best used as a storage type, not an operational type. By that I mean, your data structures can have fields whose values may be missing, but when you're about to do something with those values, they’re not supposed to be missing (because if they were, you wouldn't be about to do something with them).

To this end, unwrapping optionals, and more importantly, failing the operation appropriately, in case the value is missing, is critical for code maintainability. A guard statement with a descriptive error thrown from its else block is far better than a silently skipped optional chain with no apparent reason for failure.

And if this is a common occurrence in the code base, then there is a need for an abstraction whose purpose will be to deal with missing values appropriately per use case. Something as simple as an optional unwrapper (even if it’s as simple as another intermediate function full of guard statements) goes a long way.

6 Likes

I wonder if an expression macro would work for conditional builds. You might start with a macro like a test assertion and rewrite each step of the optional chaining when you’re creating an audit build, but leave the expression untouched most of the time.

Even before the rest of the discussion, my question would be, why are you using optionals to model such load bearing data? If you feel burdened by simple unwrapping, why are you using optionals at all? It seems like you need better modeling rather than more Optional functionality.

3 Likes