@unrequired in function signature for optional closure parameters

I think it'd be reasonable for optional closure parameters to be non-escaping by default. We could only do that in a future language mode, though, e.g. when using the Swift 6 language mode; and of course we'd need to actually teach the compiler to understand that optional values can propagate non-escaping-ness.

9 Likes

It would probably be the cleanest solution. Would you allow this propoagation behaviour to be adopted by other value types, like it was proposed here [PITCH] @unrequired in function signature for optional closure parameters by @adtrevor ?
Or would you constraint this behaviour just to Optional?

Wouldn't it be enough to soothe the main concern which has been expressed: optional closures?

Yes, it could be useful to be able to tell that any type which holds closures can itself be escaping or non-escaping, as suggested here. But this would add a feature, not fix a confusion. Today I think we're mostly talking about fixing a confusing situation. That's my current interpretation.

Yes, it was my initial pitch, but @adtrevor expressed what I think is a valid point. This confusion could potentially be extendable to other types. Even tough I can't find an actual use case when this wouldn't allow a dangerous pattern. Personally, I think that just for optional would be enough to address the pitch.

Non-escaping-ness cannot be generally extended to other types besides Optional because escape tracking requires some basic assumptions about the type's nature and behavior which the language cannot reasonably prove for an arbitrary type. Also, escape tracking may become meaningful for more types than function types in the future (for example, it's might be useful to take advantage of escape tracking for class references), and we shouldn't conflate these issues in pursuit of a generalization with unclear benefit now.

2 Likes

Like I thought. Yes, I agree. I think that this solution will have no impact on ABI stability, but it will impact API Resilience, am I correct? Devs will have to change all their code where optional closures are used, as the compiler will stop them from compiling their code.

The need for parentheses is really just a disambiguation tool, and has little to do with closures, other that the fact that the way we express closure types are open for ambiguation.

  • () -> ()? represents a non-optional closure that returns an optional Void?
  • (() -> ())? represents an optional closure that returns a non-optional Void

These are different types. But there isn't really anything special about how we express them, imho.
If you use a typealias you don't need the parentheses. I often define a few well-known aliases for these common closure types:

typealias VoidClosure = () -> ()
typealias CallbackClosure<A> = (A) -> ()
typealias ResultCallback<A> = CallbackClosure<Result<A, Error>>

func executeAsync(_ work: @escaping VoidClosure) { ... }
func fetchData(from url: URL, callback: @escaping ResultCallback<Data>) { ... }
func animate(_ view: UIView, from: Point, to: Point, completion: VoidClosure?) { ... }
1 Like

I'm well aware of this. My point in that post is that it doesn't look nice, and you can't write a typealias for each closure you might have.

Anyway, this might be part of a future debate. If we can have optional that propagates escapability, then I'm already happy.

1 Like

By using generic typealiases, you can quite reasonably write a typealias for almost every single closure type you need. Almost every closure is a callback closure that takes less than four parameters, and return void.

typealias CallbackHandler<A> = (A) -> ()
typealias CallbackHandler<A, B> = (A, B) -> ()
typealias CallbackHandler<A, B, C> = (A, B, C) -> ()
typealias CallbackHandler<A, B, C, D> = (A, B, C, D) -> ()

// ...

func fetchData(from url: URL, callback: @escaping CallbackHandler<URLResponse, Data, Error>)
func requestAccess(callback: @escaping CallbackHandler<Bool>)
func dismiss(completion: CallbackHandler<Void>? = nil)
func readFile(path: String, completion: @escaping CallbackHandler<Result<Data, Error>>)
func subscribe<E: Event>(to event: E, @escaping CallbackHandler<E.Payload>)

Whatever closure type isn't covered by these simple typealiases, can probably be defined at the top of each file where they are used. It's probably just a handful of exceptions, mostly transforms.

1 Like

Well, changing a function parameter to be non-escaping is changing the contract on the function, which means:

  • It's potentially source-breaking for both implementations and clients of the function.
  • It's ABI-breaking for existing implementations of the function that don't adopt an annotation when upgrading to the new language mode.

These considerations might make it infeasible; we'd have to see practical impact. We also need better ABI-verification tooling in general.

5 Likes

I see. Would the @unneded be a viable solution? We could deprecate the use of optional, so the current contacts would not be broken.

Having the compiler telling me

Warning: to declare an optional closure, you must not use `Optional` but  `@unneded`.

would look like a language design failure IHMO.

3 Likes
WARNING: Use of optional with closures is deprecated. Use @unneded instead

Yes, it looks like a language design failure. But to deny that this is in fact a design issue is not any better. It's undeniable that right now optional closure are always escaping the function scope, without potentially really escaping it. The documentation is making a wrong statement or a wrong assumption (there are no optional closures in the world). So how do we solve this?

To teach the compiler that optional can propagate the escapability of the closure makes the proposal ABI-breaking, therefore it might never be approved. To introduce @unneded / @unrequired deprecating the use of optional with closures would not break ABI, but it looks bad... then should we fix the documentation and add a clause for optional closures? (it still look like a language design failure imho).

One thing I believe is sure is that we should do something about it.

Can you explain why this would be the case? My understanding is that the compiler's assumption that closures are not escaping allows optimisations within the function body. Is there really a difference to the way such parameters are passed in?

In any case, I'd rather have optional closure parameters that were marked as @noescape than the proposal to add a new keyword and forbid a very natural form. It would be extremely incongruous to forbid optionals from containing closures, and even more incongruous if only function parameters could not be optional closures.

@John_McCall explain the reasons in this post. While I agree that making optional propagating escapability would be the way to go, it's unlikely to be approved.

Right now optional closures are escaping always. Making them non escaping will break contracts of existing apis. Asking devs to setup @escaping keywords for their optional blocks now is in small a migration that we saw happening with swift 2 -> 3 (never again!!!) and we want to avoid it. While we could just have a step in the migrator that would add @escaping for all the existing optional closures to get an easy migration, ABI stability would still be compromised and I understand that after swift 5 this is unacceptable.

Ways to avoid... well I proposed 2 of them. @unrequired deprecating optional for closure (still supporting it tough), or change of the documentation that right now is making an untrue/incomplete statement.

If we agree that this is an issue and if we agree that we want to solve it, these two are my proposed solutions. This doesn't mean that they are the only ones. Waiting for the community to comment them, or propose something better.

That comment does not explain. It only makes the same assertion. Why would changing the parameter from/to escaping be an ABI-incompatible change? Are parameters passed differently?

In any event, I still think that documenting an optional closure parameter as @nonescaping would be better than the current proposal in this thread. Obviously this would require the compiler to be able to verify such a thing, but as it currently considers the reverse situation for non-optionals an error, it seems this should not be much of an obstacle.

As an aside: the idea of the compiler requiring an explicit annotation for something it already can verify is the requirement of the override keyword. If you leave it out, the compiler will not allow the (re)declaration in a subclass. Same here: the compiler can be taught to verify whether the optional's value is non-escaping, but require the annotation to get that behaviour in the generated code.

I'm not sure there is difference in the calling, but at least they don't use the same name mangling.

I’m not sure I agree.

Can I ask you why?

The documentation says that a closure is escaping if it will be called after the method return. Optional closures are always escaping, but not always they are called after the function's return => the documentation is incorrect. with what do you disagree?

Because I don’t see many use cases for non-escaping optional closures. And in the rare cases you really need them, is it crucial to communicate to either the compiler or the API user that the function is in fact non-escaping? And in those very few cases where it is, you can simply use a default constant Noop closure.

Terms of Service

Privacy Policy

Cookie Policy