Swift Programming: Escaping Closure - The One That Got Away

Sometimes, when looking at Swift source codes I would see something like this:

public func observe(_ action: @escaping Signal<Value, Error>.Observer.Action) 
-> Disposable? 
{...}

Gasp! WTF is that?! Swift 5 code is already very hard - as is. Now this! This '@escaping'! Escaping what? Escaping who?! Escaping where?! Escaping when? 🥴 This was when I plopped my head back, looked at the ceiling and started thinking about that little cafe business I've always wanted to open down the leafy suburban street ...

Recomposing myself. It's time to look at this closely.



Swift's document states that '@escaping' keyword marks an input argument to be Escaping Closure.

"Oh, sure. Yes! yes! Ok. That's very clear. Hmm, yes. Escaping Closure. Cool. Hmm. Very cool",

Never mind, let's look a bit more. From Swift's Programming Language: Escaping Closures, it says:
"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..."
So there you are! I knew I am very smart. So, there are possibilities that a closure is called after the enclosing function is long gone - it's about the differences of the lifespan between the 2 entities:

  1. the caller function (or the parent or the container block); and 
  2. the function/block/closure that is being called.

I can think of 2 scenarios when this happens:

  1. In asynchronous operation, as a handler that will be called at a different time
  2. As a closure that is stored somewhere and will be called sometime later - after the parent function has exited

Escaping Closure as Handler in Asynchronous Operations

We see a lot of these calls in multi-thread or concurrent operations. In this scenario, a function dispatches a long-running operation to be executed on a different thread or a different queue. The function, then, returns control of operation straight away - without waiting for the operation to be completed.

func longRunningOp(_ opName: String, delaySecond: Int) {
    print("\(opName): enter")
    let dispatchQueue = DispatchQueue(label: opName)
    dispatchQueue.async {
        print("\(dispatchQueue.label): start long running process...")
        usleep(1000000 * UInt32(delaySecond))
        print("\(dispatchQueue.label): completed long running process")
    }
    print("\(opName): exit")
}

longRunningOp("a", delaySecond: 10)
longRunningOp("b", delaySecond: 3)

Run the above code in Playground. You should see output as below

a: enter
a: exit
a: start long running process...
b: enter
b: start long running process...
b: exit
b: completed long running process
a: completed long running process

Now, let's say, instead of the 'print' statements, we want to use closures/functions to do this task for us. We can modify our code as:


func announceMsg(_ msg: String) {print(msg)}

func longRunningOp(_ opName: String,
                   delaySecond: Int,
                   announcer: (String) -> (),
                   escapingAnnouncer: @escaping (String) -> ()) {
    announcer("\(opName): enter")
    let dispatchQueue = DispatchQueue(label: opName)
    dispatchQueue.async {
        escapingAnnouncer("\(dispatchQueue.label): start long running process...")
        usleep(1000000 * UInt32(delaySecond))
        escapingAnnouncer("\(dispatchQueue.label): completed long running process")
    }
    announcer("\(opName): exit")

}

For demonstration purposes, I chose to pass 2 argument-handler functions:

  1. A handler (or a function) 'announcer' to be called inside the function as per normal uses
  2. A handler/function 'escapingAnnouncer' to be called in the dispatched queue


longRunningOp("A2",
              delaySecond: 10,
              announcer: announceMsg(_:),
              escapingAnnouncer: announceMsg(_:))

As you can see, I passed in exactly the same function block 'announceMsg(_:)' for both cases, the @escaping keyword instructs the compiler to preserver the states appropriately for each usage. Let's try one more example.

longRunningOp("B2",
              delaySecond: 3,
              announcer: {(msg) in print("(closure) \(msg)")}
    ) {(msg) in print("(escaping closure) \(msg)")}

This time we passed in a closure (nameless function) for 'announcer' and another closure for 'escapingAnnouncer'. Note, the last argument is passed in as a trailing closure where you don't need to specify the name of the last argument to the function. Let's see how it runs.

A2: enter
A2: start long running process...
A2: exit
(closure) B2: enter
(escaping closure) B2: start long running process...
(closure) B2: exit
(escaping closure) B2: completed long running process
A2: completed long running process

As you can see, the calls to function 'longRunningOp(...)' enters and exits almost immediately. Whereas the asynchronous handler functions are called at a much later stage. In this case, they were called after the parent function has long gone. Notice the Xcode is smart enough to identify an argument as needs to be Escaping and will complain with compile error-message if you omitted the '@escaping' keyword.

Escaping Closure as Stored Operation to be Called Later

Another scenario where an closure can be called - after the parent function has exited - is when the closure is stored within some variable that is accessible outside the scope of the function - for example, a closure can be stored inside an array defined at the global scope.

var funArray: [(String) -> ()] = []

func announceMsg(_ msg: String) { print(msg) }

func addMoreFun(_ someFun: @escaping (String) -> ()) {
    funArray.append(someFun)
}

addMoreFun(announceMsg(_:))
addMoreFun {(msg) in print("So much \(msg)")}

And some where, much later, these stored code blocks or functions can be executed. For example:

for fun in funArray {
    fun("Fun")
}

Run the above code in Playground, you should see:

Fun
So much Fun

You see, the stored closures are executed at a much later time - after the function itself has long gone. Again, Xcode is smart enough to notice this and will complain with compile error if you omitted the '@escaping' keyword.

Final Words

I think Swift handle Escaping Closure elegantly. The language,  the choice for keyword is appropriate  and makes your codes clear, concise, and beautiful. Come to think of it, Escaping Closure wasn't that hard to understand. Just that '@escaping' keyword that threw me off in the wrong direction. It would have been easier to understand if they changed the keyword to, for example:
  • @closure_that_will_be_use_later
  • @closure_that_will_be_use_after_the_parent_function_is_long_gone
  • @closure_that_outlast_enclosing_function
  • @see_you_later_closure
  • @sleeping_handler
  • @zombie_handler
  • ...
Or maybe just leave it as '@escaping'. Yes?




No comments:

Post a Comment