[swift-evolution] Sketch: Teach init a 'defer'-like ability to deinit

Brent Royal-Gordon brent at architechies.com
Sat Jun 11 04:30:10 CDT 2016


> 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



More information about the swift-evolution mailing list