Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

swift

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:

  • Compiler raise an error if self is not specified: error: reference to property 'callbackQueue' in closure requires explicit 'self.' to make capture semantics explicit
  • To prevent compiler error I am specifying weak reference to self.

In method loadHappinessV2:

  • I decided to introduce two nested functions and simplify the "body" of operation.
  • Compiler does not raise error about capture semantics.

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 .(")
   }
}
like image 235
Vlad Avatar asked Aug 11 '16 20:08

Vlad


1 Answers

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 .(")
   }
}
like image 151
Vlad Avatar answered Nov 15 '22 18:11

Vlad