Add nil-collapsing call expression

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.

If throw becomes an expression of type Never and Never becomes a true bottom type, you'd be able to write this as:

   try? quixify(foo: acquireFoo() ?? throw CannotAcquireFoo, 
                bar: acquireBar() ?? throw CannotAcquireBar,
                baz: acquireBaz())

Of course you could also use plain try and handle the errors explicitly as well.

4 Likes

I like that. It feels a good bit more elegant than adding more syntax.

A couple questions:

  1. Is there any penalty or overhead to using exceptions at runtime (apart from the cost of allocating the error)?

  2. Is throw currently at all likely to become an expression? I'm out-of-the-loop as to any current or past discussions about that.

This topic was discussed just last week by core team members: Unwrap or Throw - Make the Safe Choice Easier - #3 by Joe_Groff.

This idiom appears nice on the surface, but it doesn't interact well with the use case where the function itself is throwing. The try? myFunc(optionalArg ?? throw MyError()) idiom swallows errors thrown by myFunc in addition to those thrown when a nil argument is encountered. Proper support requires a feature that is orthogonal to the error system, just like optional chaining is orthogonal.

FWIW, the general structure underlying a solution to this problem is Applicative. Some programming languages include syntactic sugar called "idiom brackets" which addresses the use case for optionals, but also provides sugar for other Applicative types such as Result, Future, etc. I'm not sure exactly what this kind of syntactic sugar would look like in Swift but I would love to see it explored and added to the language someday.

1 Like

Great timing, I had just now found that topic and started reading it.

That is a very good point. One way to get around it that isn't too horrible would be to separate building the argument list and applying the function to it. Tuple-splatting was removed (for what appear to be very good reasons), and propagating labels on named tuples can behave a little oddly, but something like this is possible:

let args = try? (foo: acquireFoo() ?? throw CannotAcquireFoo, 
                 bar: acquireBar() ?? throw CannotAcquireBar,
                 baz: acquireBaz())
do {
  try quixify <~ args // where <~ is some kind of splat/apply
} catch {
  // ...
}

This is less than great though. Maybe this kind of behavior is best left off for when we have variadic generics? It could possibly just be implemented as a higher-order function (modulo named parameter weirdness). There are some downsides to that too though. In the case where the function in question is an enum case initializer, you lose inference before the dot.

func foo() -> MyEnum {
  // this doesn't work, you have to write MyEnum.fizz
  return nilCollapsify(.fizz)(/* snip: arguments */) 
}

Somehow I don't think we're likely to see bananas, lenses and barbed wire in Swift any time soon :sweat_smile:.

Well, developing on the generalization of control flow as expressions discussed in that thread, you could perhaps do something like:

label: do {
  myFunc(optionalArg ?? break label)
}

or:

let optional = { myFunc(optionalArg ?? return nil) }()

that wouldn't trample on the error handling path. Idiom brackets are nice (though not as nice as extensible effects IMO), but for Either-like effects where there is no reentrance into the computation, I think the approach of control-flow-as-expression is more approachable.

4 Likes

I would love to have extensible effects and forced to choose between them would agree with you. I have always thought of them as addressing slightly different use cases though. How might a solution to this use case look with extensible effects?

That's not too bad but it seems a little on the clever side and also a little verbose for such a common use case. On the other hand, I can see the argument that general purpose sugar might be harder to understand than something more explicit like this. That makes me wonder if maybe optional does deserve the same kind of special case treatment here that it gets in many corners of the language.

If we do rely on the control-flow-as-expression approach I think that would increase the importance of coalescing optional in try? as discussed here: Make try? + optional chain flattening work together. This would improve the experience of the idiom in optional-returning functions (both non-throwing and throwing in cases where it's ok to flatten errors into nil). Without making try? coalesce optional the idiom would produce a double optional for optional-returning functions which is not what would intended or desired.

1 Like

A while back there was talk of using ? after a function name to convert it to take one level higher of optional for each parameter, and of the return value as well... with it short circuiting and returning nil if any of the parameters were nil.

func foo(a: Int, b: String) -> Int

Which could be called as:

foo?(a: 10, b:nil) //has signature foo(a: Int?, b: String?) -> Int?

The worry was that both the short circuiting behavior and double optionals (e.g. Int??) would confuse people.

How about using optional pattern matching syntax:

func foo(_ x: Int, _ y: Int) -> Int {
    return x + y
}

let a: Int? = 5
let b: Int? = nil

foo(a?, a?) // => 10
foo(a?, b?) // => nil
foo(b?, a?) // => nil
3 Likes

That would create a couple of nice behaviors. I initially discarded it because I thought it would conflict with existing syntactic forms, but it doesn't seem like it would, actually.

  1. You can collapse on some rather than all parameters.
let c: Int = 10

foo(c, a?) // => 15
  1. This syntax would also intuitively generalize to operator expressions (which are conceptually sugared function calls).
a? + a?  // => 10
a? + b?  // => nil

This mixes nicely with coalescing:

a? + (b ?? 0) // => 5
b? + (a ?? 0) // => nil

This makes it quite succinct to indicate which parts of an expression are "required" for the computation, and which can be defaulted.

  1. (Stretching) you could extend the same sugar to other expressions while preserving intuition, e.g.
let a: Int? = [1, 2, 3, nil]

for case let x? in a { print(x) }
// becomes
for x? in a { print(x) }

There was discussion about this in another thread I believe, it would be neat to see the same syntax work throughout the language. On the other hand, this is overloading ? to mean a lot of related things. Not that it isn't already of course...

If someone wants to look through the mailing list archives, I'm sure you can find several mentions of this idea. Might be good to see where those conversations went before starting down this path again from scratch.

In regard to bob, I'm pretty sure someone has come up with what I want to say before, but personally, I currently don't have the time to crawl mailing lists :grimacing:

I basically just want to say, that I don't find the current syntax demonstrated above (that can be collapsed a bit) not too bad at all:

func tryQuxify() -> Qux? {
  // You need the type notation at the end to avoid an "initializer for conditional binding must have Optional type" compiler error
 // There is currently another thread about this topic
  guard let foo = tryAcquireFoo(), 
        let bar = tryAcquireBar(), 
        let baz: Baz = tryAcquireBaz() else { return nil }
  return quxify(foo: foo, bar: bar, baz: baz)
}

This code seems "natural" to me in swift.

Also this code seems not to be significantly longer than what is described above.

Sure, you can get nil collapsing inside one line, e.g. with <*> (see: Haskell, Applicative), but to me the above code also feels more intuitive (coming from the context of imperative languages) than a long chain of applications. If there is a nice and intuitive way to write such a chain of applications (in contrast to the basic<*> notation which isn't intuitive for most people I know), I think it would be a great idea to have it in swift, not only for Optional, but the whole concept might be to abstract...?

I think if someone has some experience in swift, the above code is well understandable. The other solutions above aren't bad at all and also not too hard to read (at least to me), but from my point of view, they seem to use less "basic" concepts of the language. This is IMO an important point, since nil collapsing is something you stumble upon relatively quickly when learning the language and getting introduced to optionals.

I wrote a thread about this here:

The pseudo-code imagines what idiom brackets may look like in Swift.

Optionals would work as proposed in this thread:

var id: Int?
var email: String?
User(|id: id, email: email|)
// Optional<User>

Throws could accumulate errors somehow.

func validate(id: Int) throws -> Int {
  if id < 1 { throw ValidationError.invalidId }
  return id
}
func validate(email: String) throws -> Int {
  if email.index(of: "@") == nil { throw ValidationError.invalidEmail }
  return email
}
User(|id: try validate(id: id), email: try validate(email: email)|)
// accumulates multiple errors in Result<User, [Error]>,
// or rethrows some accumulated error type

Async/await could optimize for parallelization:

User(|id: await fetchId(), email: await fetchEmail()|)
// dispatches "fetchId" and "fetchEmail" concurrently

I must have forgotten about your previous post and the imagined syntax. I like it! :slight_smile: If we want to push this forward before higher-kinded types are available maybe we could explore a design for idiom brackets that uses an @applicative attribute, similar to where @dynamicMemberLookup landed.

I really like the syntax sugar of foo(a?, b?). It falls inline with the forced unwrap operator: foo(a!, b!)

The main issue is short circuiting. By convention, I don't use functions with side effects as direct inputs to other functions, so I feel we should short circuit is preferred.

1 Like

Isn't the canonical way to do this today Optional.map?

func foo(_ x: Int, _ y: Int?) -> Int { return x + (y ?? 0) }

let x: Int? = nil
let y: Int? = 3

y.map { foo($0, 3) } // == .some(6)
x.map { foo($0, 3) } // == nil

tryAcquireFoo().map { foo in tryAcquireBar().map { bar in tryAcquireBaz().map { baz in quxify(foo, bar, baz) } } }
1 Like

Map does work, however it is not as clean and is more difficult to understand if there are multiple optionals. It also nests the optionals, which requires an as? to unwrap the Int??? to an Int? if there are more then 1 maps.

let x: Int? = 1
let y: Int? = 2
let z: Int? = 3

func foo (_ values: Int...) -> Int {
    return 6
}

//canonical way
let s = x.map { x in y.map { y in z.map { z in foo(x, y, z)}}} as? Int

//pitched way
let s = foo(x?, y?, z?)

If you want to flatten the nested optionals you use flatMap instead of map

let s = x.flatMap { x in y.flatMap { y in z.map { z in foo(z, y, z) }}}

Still not great.

This seems like a “zip with” operation on Optional to me. Not provided by the standard library right now, but I’ve written these functions for myself before.

zip(optionalA, optionalB) { foo($0, $1) }

This evaluates to nil if either of its inputs are nil or the result of calling foo otherwise.

Not entirely sure this is an appropriate use of the name “zip” but I have not found better naming before and it feels analogous to zipping on an array in the monadic sense.

Could even be variadic so that you could write:

zip(optionalA, optionalB, optionalC) { args in foo(args[0], args[1], args[2]) }