Allowing @escaping for optional closures in method signature

From a perspective of a framework writer it is important to provide clear and understandable documentation for the correct usage of their APIs. Swift allows us to write functions that are at some extent self documenting, and clearly define what the function will actually do.

since swift 3, closures in function parameters are by default nonescaping: you pass a closure as function parameter, and you know that this closure will be synchronously used within the function scope before this function returns.

declaring the closure to be @escaping as function parameter we say:

A closure is said to escape a function when the closure is passed as an argument to the function, but is called after the function returns . When you declare a function that takes a closure as one of its parameters, you can write @escaping before the parameter’s type to indicate that the closure is allowed to escape.

Quote from Swift documentation

func nonescaping(closure: () -> Void)
func escaping(closure: @escaping () -> Void)

But for optional closures, things are different.

func foo(closure: (() -> Void)? = nil)

Here we are saying that this closure parameter is not essential for the function to work, but what about its escaping nature?

In this case the closure is implicitly escaping because of the definition of Optionality in swift. But is that closure intended to escape the actual function scope? Is this closure executed before or after the function return?

Just by reading the function signature this is unknown, and we are forced by the compiler to assume that it is escaping the function.

From this point of view

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

Is the same as

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

except that closure is not escaping foo in the second case, and if it wasn't for the definition of Optional it wasn't escaping foo in the first case either.

In this parallel pitch @John_McCall gave us an important information:

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.

Ideally, I believe that Optional closures should behave exactly the same as normal closures when passed as parameters. Meaning non escaping by default, with possibility to declare them @escaping. This would allow an uniform experience while dealing with closures in functions as parameters, regardless the fact that they are optional or not.

While I think it's the right thing to do, I also understand that to change the default now would mean to change the contract in existing functions and this would be source breaking for both implementations and clients of the functions.

I think that either

func foo(completion: @escaping ((Bool) -> Void)? = nil) // with default non escaping

or

func foo(completion: @nonescaping ((Bool) -> Void)? = nil) // with default escaping

should be allowed by the swift compiler.

What do you think guys?

4 Likes

I think it'd be a bit redundant. My mental model (and the logical model) for this has always been that () -> () and (() -> ())? are actually very different types. One is the type () -> (), and the other is the type Optional<() -> ()>.

Putting aside how the actual final assembly of how Optional is represented, it's fundamentally an enum with a case some(Wrapped). And since it has a (stored) associated value, the function is already effectively an escaping function. It's up to the optimizer at that point to realize that the optional function won't actually escape, and thus maybe enable some better optimizations.

I see it as an extension of why we do:

struct Thing {
  var t: () -> ()
}

and not

struct Thing {
  var t: @escaping () -> ()
}

And still be able to do things like:

let a: Thing

do {
  var b = 0

  a = Thing {
    print(b)
    b += 1
  }
}

a.t()
a.t()

Really this boils down to Swift (and most programming languages that offer closures) not really providing an ultra-strict difference between what an anonymous function is vs what a closure is (for good reasons). If Swift enforced the distinction between anonymous functions and closures in every place, the language would be much more verbose.

2 Likes

FWIW, this works today:

var bar: ((Bool) -> Void)?

func foo(completion: ((Bool) -> Void)? = nil) {
  bar = completion  // so completion is implicitly @escaping
}

And you do still get the desired warning:

class Frotzle {  
  func bongle() { 
    foo {
      brazzle($0)  // error: call to method 'brazzle' in closure requires explicit 'self.' to make capture semantics explicit
    }
  } 
   
  func brazzle(_ x: Bool) { } 
} 

So maybe the language already does what you want?

Aside: This seems tangentially related to the oft-requested “weak blocks or blocks that weakly retain self or something like that” feature.

but not all the optional closures are escaping. for self documenting purposes there must be a way to differentiate these cases. don't you think so?

By virtue of the definition of Optional, they are effectively escaping AFAIU.

2 Likes

not if you don't store the optional for later usage. For memory management purposes the optional is deallocated at completion of the scope, and with it the closure also.

1 Like

Ah, so the wish here is to support non-escaping optional closures.

2 Likes

Agree.

I think what you're looking for is a way to accept an optional closure parameter that is guaranteed but the compiler to not escape the call stack. There is no way to do that in the language today.

we can put it this way, yes. I'm pitching a way to get @escaping and not @escaping.

Sorry if my initial post was unclear about that. I left implicit that

func foo(completion: @escaping ((Bool) -> Void)? = nil) is escaping and
func foo(completion: ((Bool) -> Void)? = nil) is not

the wish is to make explicit the escaping nature of the block. :slight_smile:

Adding @escaping would be redundant. It already is escaping because it's stored in an associated value and not a direct parameter.

1 Like

This makes sense, but don't you think that there should be a way to define optional blocks as non escaping? (besides passing empty blocks as default... that makes the method ugly to read)

I think it's reasonable to want something like this but it would require very nontrivial changes to the language.

1 Like

Fair enough. :) I understand.

FWIW, the status quo is the escape hatch withoutActuallyEscaping(_:do:)

My goal is to make it explicit to the method user, not to who writes the method.

You can wrap it inside a function in all the ways imaginable, no?
Though tbh, I'm don't know what you're trying to achieve.

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.

Terms of Service

Privacy Policy

Cookie Policy