Vlad
Vlad

Reputation: 6732

Swift: Capture semantics when calling nested function from closure. Why compiler does not raise error?

Need your help in getting understanding how Swift capture semantics working when nested function called from closure. So, I have two methods loadHappinessV1 and loadHappinessV2.

In method loadHappinessV1:

In method loadHappinessV2:

Why in method loadHappinessV2 compiler does not raise error about capture semantics? Are the nested functions (together with variable callbackQueue) not captured?

Thanks!

import PlaygroundSupport
import Cocoa

PlaygroundPage.current.needsIndefiniteExecution = true

struct Happiness {

   final class Net {

      enum LoadResult {
         case success
         case failure
      }

      private var callbackQueue: DispatchQueue
      private lazy var operationQueue = OperationQueue()

      init(callbackQueue: DispatchQueue) {
         self.callbackQueue = callbackQueue
      }

      func loadHappinessV1(completion: (LoadResult) -> Void) {
         operationQueue.cancelAllOperations()

         let hapynessOp = BlockOperation { [weak self] in
            let hapynessGeneratorValue = arc4random_uniform(10)
            if hapynessGeneratorValue % 2 == 0 {
               // callbackQueue.async { completion(.success) } // Compile error
               self?.callbackQueue.async { completion(.success) }
            } else {
               // callbackQueue.async { completion(.failure) } // Compile error
               self?.callbackQueue.async { completion(.failure) }
            }
         }
         operationQueue.addOperation(hapynessOp)
      }

      func loadHappinessV2(completion: (LoadResult) -> Void) {
         operationQueue.cancelAllOperations()

         func completeWithFailure() {
            callbackQueue.async { completion(.failure) }
         }

         func completeWithSuccess() {
            callbackQueue.async { completion(.success) }
         }

        let hapynessOp = BlockOperation {
            let hapynessGeneratorValue = arc4random_uniform(10)
            if hapynessGeneratorValue % 2 == 0 {
                completeWithSuccess()
            } else {
                completeWithFailure()
            }
         }
         operationQueue.addOperation(hapynessOp)
      }
   }
}

// Usage
let happinessNetV1 = Happiness.Net(callbackQueue: DispatchQueue.main)
happinessNetV1.loadHappinessV1 {
   switch $0 {
   case .success: print("Happiness V1 delivered .)")
   case .failure: print("Happiness V1 not available at the moment .(")
   }
}

let happinessNetV2 = Happiness.Net(callbackQueue: DispatchQueue.main)
happinessNetV2.loadHappinessV2 {
   switch $0 {
   case .success: print("Happiness V2 delivered .)")
   case .failure: print("Happiness V2 not available at the moment .(")
   }
}

Upvotes: 8

Views: 1757

Answers (2)

Vlad
Vlad

Reputation: 6732

I found some explanation how capture semantics working with nested functions. Source: Nested functions and reference capturing.

Consider following example:

class Test {

    var bar: Int = 0

    func functionA() -> (() -> ()) {
        func nestedA() {
            bar += 1
        }
        return nestedA
    }

    func closureA() -> (() -> ()) {
        let nestedClosureA = { [unowned self] () -> () in
            self.bar += 1
        }
        return nestedClosureA
    }
}

Compiler reminds us to maintain ownership in function closureA. But does not tell anything about capturing self in function functionA.

Lets look on Swift Intermediate Language (SIL):
xcrun swiftc -emit-silgen Test.swift | xcrun swift-demangle > Test.silgen

sil_scope 2 { loc "Test.swift":5:10 parent @Test.Test.functionA () -> () -> () : $@convention(method) (@guaranteed Test) -> @owned @callee_owned () -> () }
sil_scope 3 { loc "Test.swift":10:5 parent 2 }

// Test.functionA() -> () -> ()
sil hidden @Test.Test.functionA () -> () -> () : $@convention(method) (@guaranteed Test) -> @owned @callee_owned () -> () {
// %0                                             // users: %4, %3, %1
bb0(%0 : $Test):
  debug_value %0 : $Test, let, name "self", argno 1, loc "Test.swift":5:10, scope 2 // id: %1
  // function_ref Test.(functionA() -> () -> ()).(nestedA #1)() -> ()
  %2 = function_ref @Test.Test.(functionA () -> () -> ()).(nestedA #1) () -> () : $@convention(thin) (@owned Test) -> (), loc "Test.swift":9:16, scope 3 // user: %4
  strong_retain %0 : $Test, loc "Test.swift":9:16, scope 3 // id: %3
  %4 = partial_apply %2(%0) : $@convention(thin) (@owned Test) -> (), loc "Test.swift":9:16, scope 3 // user: %5
  return %4 : $@callee_owned () -> (), loc "Test.swift":9:9, scope 3 // id: %5
}

The line strong_retain %0 : $Test, loc "Test.swift":9:16, scope 3 // id: %3 tells us that compiler making strong reference for $Test (which is defined as self), this reference lives in scope 3 (which is functionA) and not released at a time of leaving scope 3.

Second function closureA deals with optional reference to self. It is represented in code as %2 = alloc_box $@sil_weak Optional<Test>, var, name "self", loc "Test.swift":13:38, scope 8 // users: %13, %11, %9, %3.

sil [transparent] [fragile] @Swift.Int.init (_builtinIntegerLiteral : Builtin.Int2048) -> Swift.Int : $@convention(method) (Builtin.Int2048, @thin Int.Type) -> Int

sil_scope 6 { loc "Test.swift":12:10 parent @Test.Test.closureA () -> () -> () : $@convention(method) (@guaranteed Test) -> @owned @callee_owned () -> () }
sil_scope 7 { loc "Test.swift":17:5 parent 6 }
sil_scope 8 { loc "Test.swift":15:9 parent 7 }

// Test.closureA() -> () -> ()
sil hidden @Test.Test.closureA () -> () -> () : $@convention(method) (@guaranteed Test) -> @owned @callee_owned () -> () {
// %0                                             // users: %5, %4, %1
bb0(%0 : $Test):
  debug_value %0 : $Test, let, name "self", argno 1, loc "Test.swift":12:10, scope 6 // id: %1
  %2 = alloc_box $@sil_weak Optional<Test>, var, name "self", loc "Test.swift":13:38, scope 8 // users: %13, %11, %9, %3
  %3 = project_box %2 : $@box @sil_weak Optional<Test>, loc "Test.swift":13:38, scope 8 // users: %10, %6
  strong_retain %0 : $Test, loc "Test.swift":13:38, scope 8 // id: %4
  %5 = enum $Optional<Test>, #Optional.some!enumelt.1, %0 : $Test, loc "Test.swift":13:38, scope 8 // users: %7, %6
  store_weak %5 to [initialization] %3 : $*@sil_weak Optional<Test>, loc "Test.swift":13:38, scope 8 // id: %6
  release_value %5 : $Optional<Test>, loc "Test.swift":13:38, scope 8 // id: %7
  // function_ref Test.(closureA() -> () -> ()).(closure #1)
  %8 = function_ref @Test.Test.(closureA () -> () -> ()).(closure #1) : $@convention(thin) (@owned @box @sil_weak Optional<Test>) -> (), loc "Test.swift":13:30, scope 8 // user: %11
  strong_retain %2 : $@box @sil_weak Optional<Test>, loc "Test.swift":13:30, scope 8 // id: %9
  mark_function_escape %3 : $*@sil_weak Optional<Test>, loc "Test.swift":13:30, scope 8 // id: %10
  %11 = partial_apply %8(%2) : $@convention(thin) (@owned @box @sil_weak Optional<Test>) -> (), loc "Test.swift":13:30, scope 8 // users: %14, %12
  debug_value %11 : $@callee_owned () -> (), let, name "nestedClosureA", loc "Test.swift":13:13, scope 7 // id: %12
  strong_release %2 : $@box @sil_weak Optional<Test>, loc "Test.swift":15:9, scope 7 // id: %13
  return %11 : $@callee_owned () -> (), loc "Test.swift":16:9, scope 7 // id: %14
}

So, if nested function accesses some properties defined in self, then nested function keeps strong reference to self . Compiler does not notify about it (Swift 3.0.1).

To avoid this behaviour we just need to use closures instead nested functions. Then compiler will notify about self usage.

Original example could be rewtitten as following:

import PlaygroundSupport
import Cocoa

PlaygroundPage.current.needsIndefiniteExecution = true

struct Happiness {

   final class Net {

      enum LoadResult {
         case success
         case failure
      }

      private var callbackQueue: DispatchQueue
      private lazy var operationQueue = OperationQueue()

      init(callbackQueue: DispatchQueue) {
         self.callbackQueue = callbackQueue
      }

      func loadHappinessV1(completion: @escaping (LoadResult) -> Void) {
         operationQueue.cancelAllOperations()

         let hapynessOp = BlockOperation { [weak self] in
            let hapynessGeneratorValue = arc4random_uniform(10)
            if hapynessGeneratorValue % 2 == 0 {
               // callbackQueue.async { completion(.success) } // Compile error
               self?.callbackQueue.async { completion(.success) }
            } else {
               // callbackQueue.async { completion(.failure) } // Compile error
               self?.callbackQueue.async { completion(.failure) }
            }
         }
         operationQueue.addOperation(hapynessOp)
      }

      func loadHappinessV2(completion: @escaping (LoadResult) -> Void) {
         operationQueue.cancelAllOperations()

         // Closure used instead of nested function.
         let completeWithFailure = { [weak self] in
            self?.callbackQueue.async { completion(.failure) }
         }

         // Closure used instead of nested function.
         let completeWithSuccess = { [weak self] in
            self?.callbackQueue.async { completion(.success) }
         }

         let hapynessOp = BlockOperation {
            let hapynessGeneratorValue = arc4random_uniform(10)
            if hapynessGeneratorValue % 2 == 0 {
               completeWithSuccess()
            } else {
               completeWithFailure()
            }
         }
         operationQueue.addOperation(hapynessOp)
      }
   }
}

// Usage
let happinessNetV1 = Happiness.Net(callbackQueue: DispatchQueue.main)
happinessNetV1.loadHappinessV1 {
   switch $0 {
   case .success: print("Happiness V1 delivered .)")
   case .failure: print("Happiness V1 not available at the moment .(")
   }
}

let happinessNetV2 = Happiness.Net(callbackQueue: DispatchQueue.main)
happinessNetV2.loadHappinessV2 {
   switch $0 {
   case .success: print("Happiness V2 delivered .)")
   case .failure: print("Happiness V2 not available at the moment .(")
   }
}

Upvotes: 7

GetSwifty
GetSwifty

Reputation: 7746

My first guess is Swift implicitly defines nested functions as either @noescape functions, or an Autoclosure. (some info here). With either of those types you don't have to use "self", and the hapynessOp block would capture the references to the nested functions, so there wouldn't be any problems there

Otherwise it may be the case that nested functions are actually added to the signature of the class. I'm thinking it's possible to do some testing and find out (May get around to that).

Upvotes: -1

Related Questions