Nil-coalescing expressions are ubiquitous in Swift. This pitch proposes an analogous expression for function calls nicknamed "nil-collapsing" (or "nil-cascading"). Instead of coalescing, it collapses the entire expression.
What?
Note: I am unsure of the best syntax, so I will use ¿ as a placeholder.
While a nil-coalescing expression a ?? b
reduces to b
when a
is nil
, a nil-collapsing expression f¿(...)
reduces to nil
when any of the non-optional arguments of f
are nil
. To explain the behavior in more detail, we consider the following example:
func foo(_ x: Int, _ y: Int?) -> Int { return x + (y ?? 0) }
foo(nil, 2) // *type error*
foo¿(1, 2) // => 3 (no collapsing behavior)
foo¿(nil, 2) // => nil (call is collapsed, x is not optional)
foo¿(1, nil) // => 1
In effect, ¿ acts over a function type by adding an extra layer of Optional to all arguments. If any of the arguments which are not already Optional are nil
, then the call expression collapses.
I have attempted to implement this in user code, but failed for a couple of reasons:
- Propagating argument labels is finicky and fails for single parameter functions.
- It's not possible to check if a type is Optional (no type traits), so one ends up needing to implement overloads for every combination of Optional and non-Optional parameters for every possible tuple size (2-7, following with the standard library).
Why?
Consider the following motivating example:
// We have three functions which may or may not provide some resource.
func tryAcquireFoo() -> Foo? { /* snip */ }
func tryAcquireBar() -> Bar? { /* snip */ }
func tryAcquireBaz() -> Baz? { /* snip */ }
// And some function that requires a couple resources and
// optionally requires one in order to produce something else.
func quxify(foo: Foo, bar: Bar, baz: Baz?) -> Qux { /* snip */ }
// To glue these all together we would have to do this:
func tryQuxify() -> Qux? {
guard let foo = tryAcquireFoo() else {
return nil
}
guard let bar = tryAcquireBar() else {
return nil
}
let baz = tryAcquireBaz()
return quxify(foo: foo, bar: bar, baz: baz)
}
// One could also use flatMap/compactMap, but that seems
// very heavy and unwieldy. At the least, the above is clear.
// But by contrast, with a nil-collapsing operator,
// the intent can be expressed concisely and clearly:
func tryQuxify2() -> Qux? {
return quxify¿(foo: acquireFoo(),
bar: acquireBar(),
baz: acquireBaz())
}
In short, a nil-collapse does what it says on the tin: it collapses an expression to nil. The term “nil-cascade” might be better though, in line with cascading behavior in a database.