Swift 6: reconsider @escaping for optional function-type parameters

This topic has come up before, but because of the source compatibility considerations I wanted to raise it again in the context of Swift 6 and the possibility of a breaking change in a new language mode. The motivation and solution are the same as discussed here. Broadly we would change the default behavior of optional function-typed function parameters so that in:

func foo(fn: (() -> Void)?) {}

the parameter fn would be non-escaping, and would require the addition of an @escaping attribute to make it escaping.

This would apply only to optional function types (perhaps at any level of optional nesting?) and would not imply any broader form of escape analysis for other values/references. In the Swift 5 language mode, @escaping would continue be the implicit default and there would be no way to spell a non-escaping optional function-typed parameter.

Previous discussions have indicated that such a change would be highly feasible, so it seems that the only remaining question to answers is whether this change is worth a source break in a new language version. With the opportunity upon us, such a discussion seems timely. Curious to hear everyone's thoughts!

33 Likes

+1
I’ve always found it curious and awkward that optional args are automatically escaping.

As it stands it just increases cognitive load and seems inconsistent.

Also annoying when I change a parameter from non optional to optional or the other way around.

6 Likes

I'm not sure it's tenable to not have a way to spell this in Swift 5 mode, but I agree that making this change would be highly desirable for Swift 6.

13 Likes

I’m obviously in favor of this change as original pitcher. :slight_smile: I’ll be happy to contribute on its implementation as much as i can

3 Likes

I'm +1 on this pitch, but think that this is a more general issue. Consider a type like Either:

enum Either<A, B> { case a(A), b(B) }

There isn't a significant structural difference between Optional and such a type — they're both generic enums that accept payload on their cases. Thus, an @noescape-type attribute for types whose payload is known not to escape seems generally useful. (I'm not sure if this is the feature described by @John_McCall or a feature that has been discussed elsewhere, so please let me know if that's the case.)

Admittedly, the sugar around optionals changes things. However, we could first introduce a general feature. This way, the implicit no-escape syntax of optional-wrapped function parameters would build on an existing feature, instead of being a niche Optional feature.

3 Likes

A general feature would be great, but I suspect significantly more complicated than the optional version of this feature, for significantly less benefit. In Swift codebases I've worked in, a large number of closures are passed as optional function-type parameters, and situations where they are assigned to, say, enum cases or struct properties without ultimately escaping are vanishingly rare.

4 Likes

You could extend it even further - to stored properties in struct and classes. Why should enums be so special?

Basically, the general feature is something like Rust's lifetime system. You have a closure, with a certain lifetime (limited to the function it was defined in), and anything which stores that closure - whether it's an enum like Optional or Either, or any other kind of storage - is also bound by that same lifetime and cannot extend it by "escaping".

That said, Optional is semi-compiler-magic anyway, so we might be able to make this one (very common) case a bit easier without a fully-general lifetime system.

4 Likes

I'm kind of horrified to find out that Optional function type parameters are implicitly escaping. This is the first I'm hearing of this. The only thing I would have expected to auto escape would be a function declared outside of the thing taking a function parameter.

Indeed, Optional and escaping seem orthogonal.

+1 to this.

1 Like

Indeed, Optional and escaping seem orthogonal.

+1 to the proposal, based on the logic in this comment from @Hacksaw.

This also follows the Principle of Least Astonishment/Surprise.

1 Like

+1 for me. I've been bitten by this in the past where I had to realize the implicit @escaping behavior for generic parameters the hard way.

func foo<T>(_ value: T) // `T` is implicitly @escaping

// hence `Optional<Wrapped>.init(...)` has an implicitly @escaping `Wrapped`
// hence `(() -> Void)?` is implicitly @escaping as well

I also second that it would be cool to explore a more general approach how to solve this problem everywhere while I'm okay if we start just with an Optional.

7 Likes

For the sake of performance and optimal default behavior, I think we should consider eventually making everything non-escaping by default. Yes, including non-closure values.

Not now, obviously. Not even in Swift 6. But maybe in Swift 7?

4 Likes

+1 for it.

I wonder should it be done with relation to [Pitch] Non-Escapable Types and Lifetime Dependency

Seems that enum Either<A, B> and similar types can also keep noescapable closures. This way Optional<T> will be a particular case.

1 Like