Sketch: Teach init a 'defer'-like ability to deinit


(Graham Perks) #1

Teach init a 'defer'-like ability to deinit

'defer' is a great way to ensure some clean up code is run; it's declaritive locality to the resource acquisition is a boon to clarity.

Swift offers no support for resources acquired during 'init'.

For an example, from https://www.mikeash.com/pyblog/friday-qa-2015-04-17-lets-build-swiftarray.html

init(count: Int = 0, ptr: UnsafeMutablePointer<T> = nil) {
    self.count = count
    self.space = count

    self.ptr = UnsafeMutablePointer<T>.alloc(count)
    self.ptr.initializeFrom(ptr, count: count)
}

deinit {
    ptr.destroy(...)
    ptr.dealloc(...)
}

Another 'resource' might be adding an NSNotificationCenter observer, and wanting to unobserve in deinit (no need in OS X 10.11, iOS 9, but for earlier releases this is a valid example).

Changing the above code to use a 'defer' style deinit block might look like:

init(count: Int = 0, ptr: UnsafeMutablePointer<T> = nil) {
    self.count = count
    self.space = count

    self.ptr = UnsafeMutablePointer<T>.alloc(count)
    self.ptr.initializeFrom(ptr, count: count)

    deinit {
        ptr.destroy(...)
        ptr.dealloc(...)
    }

    // NSNotificationCenter example too
    NSNotificationCenter.defaultCenter().addObserver(...)
    deinit {
        NSNotificationCenter.defaultCenter().removeObserver(...)
    }
}

The need to provide a separate implemention of deinit is gone. Reasoning for 'defer' applies here. There is good locality between what was initialized and what needs cleaning up.

Considerations:
1. Should deinit blocks be invoked before or after code in an explicit deinit method?
2. Should deinit blocks be allowed in other methods; e.g. viewDidLoad()?
3. How should deinit blocks be prevented from strongly capturing self (thus preventing themselves from ever running!)?

Cheers,
Graham Perks.


(Erica Sadun) #2

I really like this idea. Spatially moving cleanup next to unsafe operations is good practice.

In normal code, I want my cleanup to follow as closely as possible to my unsafe act:

let buffer: UnsafeMutablePointer<CChar> = UnsafeMutablePointer(allocatingCapacity: chunkSize)
    defer { buffer.deallocateCapacity(chunkSize) }

(Sorry for the horrible example, but it's the best I could grep up with on a moment's notice)

I like your idea but what I want to see is not the deinit child closure in init you propose but a new keyword that means defer-on-deinit-cleanup

self.ptr = UnsafeMutablePointer<T>(allocatingCapacity: count)
    deferringDeInit { self.ptr.deallocateCapacity(count) }

Or something.

-- E
p.s. Normally I put them on the same line with a semicolon but dang these things can be long

···

On Jun 8, 2016, at 10:54 AM, Graham Perks via swift-evolution <swift-evolution@swift.org> wrote:

Teach init a 'defer'-like ability to deinit

'defer' is a great way to ensure some clean up code is run; it's declaritive locality to the resource acquisition is a boon to clarity.

Swift offers no support for resources acquired during 'init'.

For an example, from https://www.mikeash.com/pyblog/friday-qa-2015-04-17-lets-build-swiftarray.html

init(count: Int = 0, ptr: UnsafeMutablePointer<T> = nil) {
    self.count = count
    self.space = count

    self.ptr = UnsafeMutablePointer<T>.alloc(count)
    self.ptr.initializeFrom(ptr, count: count)
}

deinit {
    ptr.destroy(...)
    ptr.dealloc(...)
}

Another 'resource' might be adding an NSNotificationCenter observer, and wanting to unobserve in deinit (no need in OS X 10.11, iOS 9, but for earlier releases this is a valid example).

Changing the above code to use a 'defer' style deinit block might look like:

init(count: Int = 0, ptr: UnsafeMutablePointer<T> = nil) {
    self.count = count
    self.space = count

    self.ptr = UnsafeMutablePointer<T>.alloc(count)
    self.ptr.initializeFrom(ptr, count: count)

    deinit {
        ptr.destroy(...)
        ptr.dealloc(...)
    }

    // NSNotificationCenter example too
    NSNotificationCenter.defaultCenter().addObserver(...)
    deinit {
        NSNotificationCenter.defaultCenter().removeObserver(...)
    }
}

The need to provide a separate implemention of deinit is gone. Reasoning for 'defer' applies here. There is good locality between what was initialized and what needs cleaning up.

Considerations:
1. Should deinit blocks be invoked before or after code in an explicit deinit method?
2. Should deinit blocks be allowed in other methods; e.g. viewDidLoad()?
3. How should deinit blocks be prevented from strongly capturing self (thus preventing themselves from ever running!)?


(Erica Sadun) #3

Twitter tl;dr:

Brent: So each instance must remember which init was used for it and then run the matching deinit code at deinit time?
Me: In my version, the constructive act and destructive act are always paired, even redundantly, using a stack if needed
Graham: so all your deferredDeinit blocks would run, no matter which init was invoked?
Brent: Closure stack in the worst case. Might be able to optimize to something cheaper if no captures. Degenerate case: `for i in 0..<10 { deinit { print(i) }

So continuing on from Twitter, assuming the compiler cannot optimize in the case of multiple inits, and init-redirections, how about allowing traditional deinit as well, and introduce compile-time optimization into traditional de-init if the compiler finds only one initialization path per class? We can also warn anyone using my version in a complicated degenerate way that it can be costly through education, manual, etc. It would also help if (especially in Cocoa), you could legally use shared initialization setup closures.

If I create an observer, I want to be able to handle its end-of-life at that point. If I allocate memory, ditto. Etc etc. Surely Swift should be able to support doing this.

-- E

···

On Jun 8, 2016, at 3:43 PM, Erica Sadun via swift-evolution <swift-evolution@swift.org> wrote:

I really like this idea. Spatially moving cleanup next to unsafe operations is good practice.

In normal code, I want my cleanup to follow as closely as possible to my unsafe act:

let buffer: UnsafeMutablePointer<CChar> = UnsafeMutablePointer(allocatingCapacity: chunkSize)
    defer { buffer.deallocateCapacity(chunkSize) }

(Sorry for the horrible example, but it's the best I could grep up with on a moment's notice)

I like your idea but what I want to see is not the deinit child closure in init you propose but a new keyword that means defer-on-deinit-cleanup

self.ptr = UnsafeMutablePointer<T>(allocatingCapacity: count)
    deferringDeInit { self.ptr.deallocateCapacity(count) }

Or something.

-- E
p.s. Normally I put them on the same line with a semicolon but dang these things can be long

On Jun 8, 2016, at 10:54 AM, Graham Perks via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

Teach init a 'defer'-like ability to deinit

'defer' is a great way to ensure some clean up code is run; it's declaritive locality to the resource acquisition is a boon to clarity.

Swift offers no support for resources acquired during 'init'.

For an example, from https://www.mikeash.com/pyblog/friday-qa-2015-04-17-lets-build-swiftarray.html

init(count: Int = 0, ptr: UnsafeMutablePointer<T> = nil) {
    self.count = count
    self.space = count

    self.ptr = UnsafeMutablePointer<T>.alloc(count)
    self.ptr.initializeFrom(ptr, count: count)
}

deinit {
    ptr.destroy(...)
    ptr.dealloc(...)
}

Another 'resource' might be adding an NSNotificationCenter observer, and wanting to unobserve in deinit (no need in OS X 10.11, iOS 9, but for earlier releases this is a valid example).

Changing the above code to use a 'defer' style deinit block might look like:

init(count: Int = 0, ptr: UnsafeMutablePointer<T> = nil) {
    self.count = count
    self.space = count

    self.ptr = UnsafeMutablePointer<T>.alloc(count)
    self.ptr.initializeFrom(ptr, count: count)

    deinit {
        ptr.destroy(...)
        ptr.dealloc(...)
    }

    // NSNotificationCenter example too
    NSNotificationCenter.defaultCenter().addObserver(...)
    deinit {
        NSNotificationCenter.defaultCenter().removeObserver(...)
    }
}

The need to provide a separate implemention of deinit is gone. Reasoning for 'defer' applies here. There is good locality between what was initialized and what needs cleaning up.

Considerations:
1. Should deinit blocks be invoked before or after code in an explicit deinit method?
2. Should deinit blocks be allowed in other methods; e.g. viewDidLoad()?
3. How should deinit blocks be prevented from strongly capturing self (thus preventing themselves from ever running!)?

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Patrick Smith) #4

I like the idea, I don’t like the potential to capture anything else though, or for instances to store closure references. It should offer similar performance to a standard `deinit`.

Instead, what about a sibling to `willSet`, `didSet` etc?

class Foo<T> {
  var ptr: UnsafeMutablePointer<T> {
    deinit {
      ptr.destroy(...)
      ptr.dealloc(...)
    }
  }

  init(count: Int = 0, ptr: UnsafeMutablePointer<T> = nil) {
    self.count = count
    self.space = count

    self.ptr = UnsafeMutablePointer<T>.alloc(count)
    self.ptr.initializeFrom(ptr, count: count)
  }
}

That way you can have as many initializers as you like. It would be called when the object deinitializes only, and not when changing the property.

This could also hopefully be possible with property behaviours.

Patrick

···

On 11 Jun 2016, at 9:45 AM, Erica Sadun via swift-evolution <swift-evolution@swift.org> wrote:

Twitter tl;dr:

Brent: So each instance must remember which init was used for it and then run the matching deinit code at deinit time?
Me: In my version, the constructive act and destructive act are always paired, even redundantly, using a stack if needed
Graham: so all your deferredDeinit blocks would run, no matter which init was invoked?
Brent: Closure stack in the worst case. Might be able to optimize to something cheaper if no captures. Degenerate case: `for i in 0..<10 { deinit { print(i) }

So continuing on from Twitter, assuming the compiler cannot optimize in the case of multiple inits, and init-redirections, how about allowing traditional deinit as well, and introduce compile-time optimization into traditional de-init if the compiler finds only one initialization path per class? We can also warn anyone using my version in a complicated degenerate way that it can be costly through education, manual, etc. It would also help if (especially in Cocoa), you could legally use shared initialization setup closures.

If I create an observer, I want to be able to handle its end-of-life at that point. If I allocate memory, ditto. Etc etc. Surely Swift should be able to support doing this.

-- E

On Jun 8, 2016, at 3:43 PM, Erica Sadun via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

I really like this idea. Spatially moving cleanup next to unsafe operations is good practice.

In normal code, I want my cleanup to follow as closely as possible to my unsafe act:

let buffer: UnsafeMutablePointer<CChar> = UnsafeMutablePointer(allocatingCapacity: chunkSize)
    defer { buffer.deallocateCapacity(chunkSize) }

(Sorry for the horrible example, but it's the best I could grep up with on a moment's notice)

I like your idea but what I want to see is not the deinit child closure in init you propose but a new keyword that means defer-on-deinit-cleanup

self.ptr = UnsafeMutablePointer<T>(allocatingCapacity: count)
    deferringDeInit { self.ptr.deallocateCapacity(count) }

Or something.

-- E
p.s. Normally I put them on the same line with a semicolon but dang these things can be long

On Jun 8, 2016, at 10:54 AM, Graham Perks via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

Teach init a 'defer'-like ability to deinit

'defer' is a great way to ensure some clean up code is run; it's declaritive locality to the resource acquisition is a boon to clarity.

Swift offers no support for resources acquired during 'init'.

For an example, from https://www.mikeash.com/pyblog/friday-qa-2015-04-17-lets-build-swiftarray.html

init(count: Int = 0, ptr: UnsafeMutablePointer<T> = nil) {
    self.count = count
    self.space = count

    self.ptr = UnsafeMutablePointer<T>.alloc(count)
    self.ptr.initializeFrom(ptr, count: count)
}

deinit {
    ptr.destroy(...)
    ptr.dealloc(...)
}

Another 'resource' might be adding an NSNotificationCenter observer, and wanting to unobserve in deinit (no need in OS X 10.11, iOS 9, but for earlier releases this is a valid example).

Changing the above code to use a 'defer' style deinit block might look like:

init(count: Int = 0, ptr: UnsafeMutablePointer<T> = nil) {
    self.count = count
    self.space = count

    self.ptr = UnsafeMutablePointer<T>.alloc(count)
    self.ptr.initializeFrom(ptr, count: count)

    deinit {
        ptr.destroy(...)
        ptr.dealloc(...)
    }

    // NSNotificationCenter example too
    NSNotificationCenter.defaultCenter().addObserver(...)
    deinit {
        NSNotificationCenter.defaultCenter().removeObserver(...)
    }
}

The need to provide a separate implemention of deinit is gone. Reasoning for 'defer' applies here. There is good locality between what was initialized and what needs cleaning up.

Considerations:
1. Should deinit blocks be invoked before or after code in an explicit deinit method?
2. Should deinit blocks be allowed in other methods; e.g. viewDidLoad()?
3. How should deinit blocks be prevented from strongly capturing self (thus preventing themselves from ever running!)?

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Brent Royal-Gordon) #5

class Foo<T> {
  var ptr: UnsafeMutablePointer<T> {
    deinit {
      ptr.destroy(...)
      ptr.dealloc(...)
    }
  }

  init(count: Int = 0, ptr: UnsafeMutablePointer<T> = nil) {
    self.count = count
    self.space = count

    self.ptr = UnsafeMutablePointer<T>.alloc(count)
    self.ptr.initializeFrom(ptr, count: count)
  }
}

I don't think this would address the stated goal of pairing initialization and deinitialization code so that one could not happen without the other.

···

On the other hand, I could imagine the *init* code also being moved into the accessor block, and then calling the init would implicitly schedule the deinit. For instance, suppose you said:

class Foo<T> {
  var count: Int, space: Int
  var ptr: UnsafeMutablePointer<T> {
    init(count: Int, ptr: UnsafeMutablePointer<T>) {
      self.ptr = UnsafeMutablePointer<T>.alloc(count)
      self.ptr.initializeFrom(ptr, count: count)
    }
    deinit {
      ptr.destroy(count)
      ptr.dealloc(space)
    }
  }

  init(count: Int = 0, ptr: UnsafeMutablePointer<T> = nil) {
    self.count = count
    self.space = count
    
    // Swift guarantees that, after a property's init returns, its deinit will always be called.
    // #property() is a placeholder for whatever syntax we use to call behavior-created methods.
    #property(self.ptr).init(count: count, ptr: ptr)
  }
}

Advantages include:

1. Initialization is broken up into per-property units, and deinitialization is paired with those units.
2. If a failable init returns during Phase 1, Swift can tell which properties have been initialized and need their deinits run. (I believe it's already doing this kind of tracking for non-custom stuff, like releasing.)
3. After phase 1, all inits have been run and all deinits are enabled, so there's no need to track anything anymore.
4. This could be used to allow stored properties in same-module extensions; we would just need to make an exception to access control so that the properties' initializers would be visible to designated initializers (and their deinits visible to the type's deinit), even outside their normal scope. Concerns would not be *quite* as separated as we might like, but it's a lot better than the "all stored properties in one file" status quo.
5. This could be used to...well, you'll see.

Disadvantages include:

1. Property inits do not know which other properties will have already been initialized, so they can't access them.
2. There is nothing to indicate the order in which property deinits should be run.
3. You can probably write multiple inits for a single property, but they'll still have to share one deinit, so this improves but does not fully solve the original problem.
4. Not as flexible as partial inits.
5. Not as flexible as a deinit that can schedule arbitrary code blocks which could vary between inits, and might even be able to be used in other methods.

Disadvantages 1 and 2 might be addressable by introducing a mechanism to constrain property init ordering. For instance, you could declare dependencies between properties:

class Foo<T> {
  var count: Int, space: Int
  
  // This property's init and deinit can access `count` and `space`, but it can only be inited
  // after them and deinited before them.
  @depends(upon: count, space)
  var ptr: UnsafeMutablePointer<T> {
    init(ptr: UnsafeMutablePointer<T>) {
      self.ptr = UnsafeMutablePointer<T>.alloc(space)
      self.ptr.initializeFrom(ptr, count: count)
    }
    deinit {
      ptr.destroy(count)
      ptr.dealloc(space)
    }
  }

  init(count: Int = 0, ptr: UnsafeMutablePointer<T> = nil) {
    self.count = count
    self.space = count
    
    #property(self.ptr).init(ptr: ptr)
  }
}

When compiling a designated initializer, the compiler would simply need to make sure these ordering rules were not violated. When synthesizing a `deinit`, the compiler would have to build a property dependency graph and then write code compatible with it.

(I suppose it would probably still be possible to write a `deinit` by hand; you would then have to write `#property(self.ptr).deinit()` yourself, and the compiler would enforce dependencies in reverse.)

It's even possible that *all* properties would have initializers—`count` and `space` would have `init(count:)` and `init(space:)` initializers, respectively. Assigning to those properties while uninitialized would just be syntactic sugar for calling the property initializers, just as assigning to any other property is syntactic sugar for calling its setter.

  * * *

Let's take this further.

It might be possible to use this to synthesize designated initializers, essentially gaining more control over memberwise inits. For instance, if you wrote this class:

class Foo<T> {
  var count: Int {
    init(count: Int = 0) {
      self.count = count
    }
  }
  
  @depends(upon: count) var space: Int {
    init() {
      space = count
    }
  }
  
  @depends(upon: count, space) // `count` here is probably redundant.
  var ptr: UnsafeMutablePointer<T> {
    init(ptr: UnsafeMutablePointer<T> = nil) {
      self.ptr = UnsafeMutablePointer<T>.alloc(space)
      self.ptr.initializeFrom(ptr, count: count)
    }
    deinit {
      ptr.destroy(count)
      ptr.dealloc(space)
    }
  }
}

The compiler might be able to look at the three property initializers:

  init(count: Int = 0)
  init()
  init(ptr: UnsafeMutablePointer<T> = nil)

And combine their signatures to create `init(count: Int = 0, ptr: UnsafeMutablePointer<T> = nil)`, then look at the dependency order to synthesize:

  init(count: Int = 0, ptr: UnsafeMutablePointer<T> = nil) {
    #property(self.count).init(count: count)
    #property(self.space).init()
    #property(self.ptr).init(ptr: ptr)
  }

By extending the rules for synthesizing property inits, this could be taken further:

1. A property with an initial value would get an `init()`. If that property had dependencies, it could use those dependencies in the initial value.
2. A property with an initial value, but marked with `@initable`, would get an `init(propertyName: Type = defaultValue)`.

(These rules could be reversed so that you mark the non-initable properties instead.)

To make the `init()` easier to use, it would be called automatically if necessary when you try to initialize another property that depends on it.

With all that in place, we can now write:

class Foo<T> {
  @initable var count = 0
  @depends(upon: count) var space = count
  
  @depends(upon: count, space)
  var ptr: UnsafeMutablePointer<T> {
    init(ptr: UnsafeMutablePointer<T> = nil) {
      self.ptr = UnsafeMutablePointer<T>.alloc(space)
      self.ptr.initializeFrom(ptr, count: count)
    }
    deinit {
      ptr.destroy(count)
      ptr.dealloc(space)
    }
  }
  
  // Synthesized for us:
  //
  // init(count: Int = 0, ptr: UnsafeMutablePointer<T> = nil) {
  // self.count = count
  // self.ptr = ptr
  // }
  //
  // Or, more explicitly:
  //
  // init(count: Int = 0, ptr: UnsafeMutablePointer<T> = nil) {
  // #property(self.count).init(count: count)
  // #property(self.space).init()
  // #property(self.ptr).init(ptr: ptr)
  // }
}

Which is kind of a startling amount of functionality, given where we started.

--
Brent Royal-Gordon
Architechies