Allowing @escaping for optional closures in method signature

The goal was, from the point of view of a framework writer to give a self documenting method signature to the framework user to say "this block is optional and not escaping" while now optional is forced to be escaping (for obvious reasons). From memory management point of view, if the block is just used within the scope of the function then it will be released when the scope of the function is finished.

Without any doubt, we would be telling to who is using this function "hey, you don't need to weakly self here". At the moment this is something you can know just by knowing how the function is implemented when a block is optional, While it is Cristal clear when the closure is not optional.

I'm really sorry if I'm not clear.

1 Like

On the other hand, you don't necessarily need an Optional in order to have a do-nothing default.

Reusing Paul's example:

func foo(completion: (Bool) -> Void = { _ in }) {
  completion(false)
}

class Frotzle {
  func bongle() {
    foo { brazzle($0) } // non-escaping
  }

  func brazzle(_ x: Bool) { }

  func byngle() {
    foo()
  }
}

For closure parameters in particular, I've found this preferable.

I haven't read the comments in this thread yet so forgive me for that. I think I asked this @Slava_Pestov once, but I don't remember if it was here in the forums or on twitter. The issue from the original post is that @escaping is implicit in generic context. This is a little bit odd and if you don't know that it can lead to bugs in your code.

I'm also not sure if this would not be a significant source breaking change because the attribute was not required before. Also you can't really add it to Optional's initializer because Wrapped is not known to be a function type.

All optional closures must be escaping, since the closure is stored inside the Optional.some case.

One could argue that it might sometimes be beneficial to be able to mark such closures as non-escaping. However, that would require some kind of language support to mark Optional as escaping/nonescaping too, or somehow add some sort of compiler magic to infer wether or not the optional that is contaning the nonescaping closure will actually outlive the caller.

This seems very non-trivial to me, but what do I know?

However, short answer is: The closure must be escaping, so no need to explicitly mark it. The same goes for instance members, etc.

@GLanza I see. I probably read a little too fast, my bad :stuck_out_tongue:.

I do like @glessard suggestion (and was about to suggest it myself if he hasn't).

To add on what others said, that it's non-trivial to add it; we don't want Optional to be special. Sure, it does have proliferated amount of sugar, but it usually ends up being normal enum with 2 cases. If we want non-escaping closure in Optional, we'd want to support it on everything else, say Foo. To make anything non-escaping, we'd need its container to be non-escaping as well, which means we want non-escaping variables.

There could definitely be a better way, but I think that's how we're likely gonna end up if we really want this feature.

I think this makes sense to add, but flipped from the OP.

I would say the spelling should be:

func foo(completion: (@nonEscaping (Bool) -> Void)? = nil)

This fixes some problems.

  1. Rather than putting the annotation on the Optional, @nonEscaping Optional<() -> ()>, it put it on the generic Type of the optional: Optional<@nonEscaping () -> ()>. I'm not sure how Functions are designed internally, but I would imagine that simplifies the implementation. Rather than allowing something to be added to an Optional-if-the-optional-is-a-function, just support it as a general annotation for functions.
  2. There should be no source compatibility issues. It's an additive change that doesn't change any existing Types
  3. If desired, supporting both @escaping and @nonEscaping to any function could be supported with no compatibility issues, and left up to the user what they want to make explicit.

The problem about the default empty closure is about how clean the method signature would look like.

func foo(completion: @escaping (Data?, URLResponse?, Error?) -> Void = { _, _, _ in })
or
func foo(completion: (Data?, URLResponse?, Error?) -> Void = { _, _, _ in })

is not pretty.

Maybe something like
func foo(completion: @escaping (Data?, URLResponse?, Error?) -> Void = Empty) ??

Don't know if it's feasible toh.

How much would it cost to extend the idea of escaping to every input? To know that an option closure was non-escaping, you'd have to track the enum (Optional) input and then use knowledge that it wouldn't escape to conclude that the closure inside it doesn't.

As long as the parameter is copyable, all of its properties are escaping. The only way to make this work is to have a special type of Optional that is non-copyable, and use it as the parameter type.

Fair point. Some additional rules would then have to be written around it, but I believe it could still be called without copying:

func foo(completion: (@nonEscaping (Bool) -> Void)? = nil) {
    completion?()
}

Unless the unwrap operation is copying the variable internally, (which I don't know), Is there a reason the same rules can't be applied as a typical non-escaping parameter? Are the mechanisms I'm unaware of to indicate that a general parameter can't be copied? If not I can imagine that being something that could/should be added to the language generally.

func empty(data: Data?, response: URLResponse?, error: Error?) {}
func foo(completion: @escaping (Data?, URLResponse?, Error?) -> Void = empty) {}

The point is that I don't want to write an "empty" function for each block I might want to be optional, just for the sake of having a prettier method signature. I was wondering if it's possible and appreciated something synthesised by the compiler, and maybe even checkable like completion.isEmpty I have no idea if it's doable. :smile:

basically
func foo(completion: @escaping (Data?, URLResponse?, Error?) -> Void = empty)
would be equivalent to
func foo(completion: @escaping (Data?, URLResponse?, Error?) -> Void = {_, _, _ in })

with corresponding non escaping ones:

func foo(completion: (Data?, URLResponse?, Error?) -> Void = empty)
func foo(completion: (Data?, URLResponse?, Error?) -> Void = {_, _, _ in })

This way there is no ambiguity around which is actually escaping.

For what it's worth, I would really like to get implicitly-ignored arguments back for empty closures. Then a pretty good option would be thus:

func foo(completion: (Data?, URLResponse?, Error?) -> Void = {})
2 Likes

FWIW, a typealias can at least smooth out the declaration.

typealias FooCompletion = (Data?, URLResponse?, Error?) -> ()

func foo(completion: FooCompletion = {_, _, _ in }) {

}

I'll open another pitch with a better intention definition

continues on [PITCH] unrequired closure parameters here I explained a bit better the reasons of this "request".

1 Like

Due to new informations I think we can continue this conversation.

@John_McCall said:

It should be quite straightforward to extend escape analysis to optional parameter functions and allow them to be explicitly marked as @escaping or @nonEscaping (or however we want to spell that). We can also investigate changing the default language rule for optional parameter functions so that it's consistent for different optionalities, but that's a separable question.

So, the take away from that parallel pitch is that we can teach the compiler to understand that optional values can propagate escapability.

I updated the original post with the new info.

1 Like

Keeping the ball rolling: We learned that optional can, with some work, propagate closures escapability.
We have at the moment two options (apart from the obvious third one of doing nothing).

  • add @nonescaping, having default optional closures as escaping (no change on default)
  • change the default of optional closures to be nonescaping.
  • none of these options. I'm happy the way closures work with optional right now.

0 voters

My personal preference would be for the second one, having uniform closure escapability handling, but I understand that it is unclear wether or not changing default for Optional closure would break ABI stability. It's something to be investigated.

Let me know your preference, please. And thank you all. :slight_smile:

2 Likes

I didn't write it explicitly, but the option "change the default of optional closures to be nonescaping" assumes that then you can mark as @escaping Optional closures just like you do with simple closures. :slight_smile:

Terms of Service

Privacy Policy

Cookie Policy