Allowing explicit @escaping for optional closures in function parameters

An Optional containing a function reference currently has that reference count as escaping, right? So why don't we keep that default and force to mark non-escaping references as needed?

func foo(closure: (() -> Void)? = nil) // escaping, like we have now
func foo(closure: @nonescaping (() -> Void)? = nil) // non-escaping

An escaping function reference can be stored, Optional instances store stuff, so an escaping default is easy to justify. If you still want to flip it, you should probably explain why in the proposal.

1 Like

In my opinion similar stuff should have similar behaviour. If function parameters are nonescaping by default, optional function parameters should too.

Also, it's very swifty to have the safer option to be the easier(or just as easy) to write, and to type more to get the one that is error prone (the opposite of c++ it seems [[nodiscard]] const __nonnull * int foo(); )

3 Likes

Flipping the default is automatically source-breaking, especially if the user likes the current escaping default.

And why is non-escaping safer than escaping? If a closure isn't escaping, it automatically can't get passed to an escaping context; that sounds safe.

1 Like

I agree. That's a big downside.

Exactly! Non-escaping functions cannot get passed to an escaping context which is safer than allowing it to do whatever, so it's swiftier for that to be the default (I'm taking about vague sense of safety, not memory safety here)

I support the proposal very warmly.

Changing the default of Optional closures from being escaping to non-escaping will introduce a a change to the contract on the function, which is potentially source-breaking for both implementations and clients of the function.

It is source-breaking, without the shadow of a doubt:

class Task {
    var completion: (() -> Void)?
    // Future compiler error: Assigning non-escaping parameter 'completion' to an @escaping closure
    init(completion: (() -> Void)?) {
        self.completion = completion
    }
    func start() { ... }
}
func doIt(completion: (() -> Void)?) {
    // Future compiler error: Passing non-escaping parameter 'completion' to function expecting an @escaping closure
    Task(completion: completion).start()
}

Maybe the proposal should admit it up front, and propose a strategy that could be discussed in the pitch and review threads:

  • Source compatibility requires that we define a new value for SWIFT_VERSION where the changes start to apply.
  • Can we have a migrator that could automate the necessary changes in user's code?
  • Don't we have to perform a review of all optional closures in the standard library, Foundation, and libDispatch?
  • Something else?
4 Likes

The regular evolution process usually rejects breaking changes, unless they are very strongly motivated.

In a normal situation, I wouldn't have bet a dime on the pitch.

But we have received some encouragement from a member of the Swift core team:

Two points in his message:

The introducing of @escaping or @nonEscaping for optional closures should be easily accepted. It does not create any breaking change, as long the default rule for optional parameter closures keeps them @escaping.

He also suggest we investigate changing the default language rule for optional parameter closures. To make them @nonEscaping by default. There lies the breaking change.

And it's quite likely he did not miss it. It's a personal interpretation, of course, and it makes me optimistic. But as I tried to explain in my previous message, a breaking change is just not a matter of implementing it. We also have to plan the transition, and this plan is part of the proposal.

3 Likes

The fact that optional closures are by default escaping is a side effect of how Optional is implemented.
We all use optionals with ? appended at the end of the variable type. We tend to forget that the underlying implementation is an associated value of an enum called Optional, and new comers probably don't even know that. I've seen seasoned developers asking "Why can't I make it escaping!" ignoring that the closure was already escaping. I've seen developers complaining "why do I have to use explicit self if it's not escaping".

This escaping thing for optional closure is something that you think obvious in the moment that you stop and think of the meaning of that ? at the end of the closure type definition, but it's not immediately obvious.

This is just an example:

On stack overflow you can find the same question.

Like @cukr said, I believe that the experience with closures should be uniform, and the usage of optional closures should converge with the usage of non optional ones.

1 Like

Will do. In which section the strategy should be explained?

As strategy I was thinking more at the good old "@objc" with the build setting SWIFT_SWIFT3_OBJC_INFERENCE something like SWIFT_SWIFT5_OPTIONAL_CLOSURES or similar.

In which section the strategy should be explained?

I don't quite know. I don't even know if there exists somewhere a checklist of things to do for a breaking change, that we could use as a reference.

Just add a "Breaking Change" section: maybe some competent people with authority will chime in and ask to "fix" the proposal structure. We'll comply then. But structure is only a support for clarity. It is clarity that is important, and we can achieve it today.

1 Like

Sorry, but could this discussion be moved to the pitch thread? It’s not relevant to the actual development (code) of the feature and this thread is for compiler development.

Yes, it looks like we still need to tune the proposal document. Sorry about that.

done

func foo(closure: @escaping () -> Void = { }) {
    ....
    closure?()
    ...
}

nitpick, there shouldn't be a question mark here

Another thing to clarify is if you will be able to make a local copy of Optional function? Do you have something in mind, or will it be just whatever is easier to implement?

func foo(closure: /*nonescaping*/(() -> Void)?) {
    let alias = closure // will this be possible?
}
func foo(closure: /*nonescaping*/() -> Void) {
    let alias = closure // currently forbidden
}

optional should completely match the behavior of non optional closures.

So, the compiler will complain with the following message Non-escaping parameter 'closure' may only be called like it happens today already for non optionals.

2 Likes

I temporarily closed the PR. The proposal is now in a gist: @escapable optional closures · GitHub and it will be updated over time with what we gather in this topic.

To be added: Migration strategy, state clearly that this is a source breaking change.

I've updated the gist. I described the possible solution in "detailed design" and stated explicitly the fact that is a breaking change in "proposed solution". Here reported the changes

Proposed solution

Escape analysis should be extended to optional parameter functions to allow them to be explicitly marked as @escaping in case they will escape the function scope.

Having Optional closures uniformed to non-optional closures behavior will reduce the confusion caused by the documentation not covering this case. Also it will provide to users more explicit and expressive APIs. Optional closures not marked as @escaping should be by default non-escaping to match the existing behavior of non-optional closures.

This is a source breaking change: Changing the default will introduce a change in the contract on the functions that have optional closures as parameters, and it is therefore source breaking for both the implementation and the clients of the function.

Detailed design

func foo(closure: @escaping (() -> Void)? = nil) // escaping
func foo(closure: (() -> Void)? = nil) // non-escaping

Having the closure to be Optional will no longer cause, as side effect, the closure to implicitly escape. The escape analysis would be extended to Optional parameter functions so that they can be explicitly marked as @escaping where appropriate.

To maintain source compatibility, a new build settings can be Introduced to keep using the legacy behaviour for the optional closures, similarly to what was previously done in swift4 migration with the SWIFT_SWIFT3_OBJC_INFERENCE build settings for the @objc automatic inference.

I also support this change. I wasn't sure it would be feasible until John's comment, but it sounds like it is. I think the proposal is correct that this is how most people who are learning Swift would expect an optional function parameter to work. Beyond that, the ability to have non-escaping optional parameters would be wonderful.

What would we be reviewing? Looking for optional function parameters that could be made non-escaping?

Ha, yes, now that you ask, I see that this step is only an optimization, and that it can be done any time later without breaking source code. I don't know about binary compatibility.

Well not really. if the default changes we would be looking all the closures that were legitimately escaping, to mark them explicitly escaping.

2 Likes

Any other suggestions on how to change the proposal ?
Which would be the next step to get it implemented? Should I partner with someone from the core team? It doesn't seem to be something that could be implemented by a beginner in swift code base. :frowning: