[swift-evolution] "selfless", init Helpers, and NSDocument

Charles Srstka cocoadev at charlessoft.com
Sat Jan 9 19:08:27 CST 2016


Okay, resurrecting this proposal (which some may not have seen due to it having been sent over the holiday break), because I thought of a really major use case.

In addition to the obvious use cases, like init methods that get too long because they can’t be broken up into smaller methods, duplicated code between init() and init(coder:), and the like, I just realized that this feature, if implemented with an appropriate annotation to make it bridgeable from Objective-C headers, could make NSDocument not suck in Swift.

Currently we have this situation:

class MyDoc: NSDocument {
	var foo: Foo!
	var bar: Bar!

	override func readFromData(data: NSData, ofType: String) throws {
		self.foo = // parse something from the data
		self.bar = // parse something from the data

		// etc.
	}
}

There are a number of problems with this.

1. Since there’s no way to know what any of the document’s state is supposed to be at the time the class is initialized, pretty much everything has to be optional. And in practice, these are going to be of the implicitly-unwrapped variety much of the time, since many of these variables have no real reason to be optional once readFromData() has been called, and no one is going to want to needlessly unwrap these things Every. Damn. Time.

2. Unlike code in init methods, the code above will call the setters for foo and bar, *not* just set the underlying ivars directly. This means that willSets and didSets, as well as things like KVO notifications, will get fired. If the document has a decent number of properties, it is easy for properties that haven’t been initialized yet to be inadvertently called as a result of the side-effects of setting properties in readFromData(), leading to a crash.

3. Redesigning NSDocument to work better with Swift is an option, except that as the language currently stands, this would be hard to do and make it work similarly to how it currently does. Particularly, the current system provides several override points. If you just need the file contents, you override readFromData(). However, if you need to access file metadata or something beyond just the contents, you can override readFromURL() instead. The default implementation of readFromURL(), of course, calls readFromData(), so you can choose which override point is more appropriate and use that. The obvious solution to this would be to turn the readFrom*() methods into initializers, but for init(URL:) to call init(data:) in its default implementation, it would need to be declared as a convenience initializer—which can’t be overridden.

If we had a way to declare methods as init helpers, a way to annotate these in Objective-C, and an annotation to put on the designated initializer indicating which init helpers the initializer will call, this might be able to be all separated out. I’m not sure whether NSDocument could be made to conform to this as is, but a hypothetical redesigned document class could look something like this:

class NSHypotheticalDocument: NSObject {
	@uses_helper(readFromURL:ofType:) init(URL url: NSURL, ofType type: String) throws {
		try self.readFromURL(url, ofType: type)
		super.init()
	}

	@uses_helper(readFromData:ofType:) init_helper readFromURL(url: NSURL, ofType type: String) throws {
		let data = try NSData(contentsOfURL: url, options: [])
		try self.readFromData(data, ofType: type)
	}

	init_helper readFromData(data: NSData, ofType type: String) throws {
		throw NSCocoaError.FeatureUnsupportedError
	}

	// other stuff
}

A subclass could now override one of the two helper methods, and since the designated initializer is documented as calling these particular helpers, the compiler could know that any properties set in an overridden helper method would be set by the end of the super.init call in the subclass’s own initializer, and thus these properties would be set without side effects. This could allow properties in document classes to be safely set without unnecessary use of optionals.

I dunno, what do you think?

Charles

> On Dec 15, 2015, at 5:59 PM, Ross O'Brien via swift-evolution <swift-evolution at swift.org> wrote:
> 
> Hi all,
> 
> I'm a new member of the list, so apologies if this is a duplicate of an existing idea or if there's already a way to do this in Swift 2.1 that I've missed.
> 
> In Objective C, and C-like languages, an initialiser function represents a stage after allocation of memory where properties are given values. In Swift, init appears to precede (or overlap with) allocation. The benefit of this is that for type-safety reasons, all properties of a type (or new properties of a derived type) can be verified as having values. The disadvantage, and one of the stumbling blocks for those who learned Objective-C, is that until all the properties have values, the instance does not exist and instance functions cannot be called.
> 
> There's an invisible threshold in Swift init() functions marking this transition. In derived classes it's the point where super.init() is called - after the derived type has provided initial values, but before any type functions can be called.
> 
> Some types have multiple initialisers, and may be duplicating a lot of code in those distinct inits before they cross the threshold. This code can't be refactored into an instance function because the instance doesn't exist yet. The instance function may not even require the use of any properties of the type.
> 
> If the compiler can read an init function and its varied control flow and determine a threshold where all properties have values, presumably it can read the code of any function called before that threshold, determine which properties they read and which they assign to, and provide a warning if a path assigns to a constant a second time, etc.. But this isn't currently happening.
> 
> I'm guessing there are multiple contributing factors for this: the combinatorial explosion of possible control flow paths with functions (particularly if they're recursive); the possibility that the function calls are used by the compiler to mark the end of a control flow path, by which point it can determine whether everything has a value; the function genuinely can't exist without allocation. I don't know the reasons but I'd be interested to learn them.
> 
> I'm proposing the keyword 'selfless' for a function which could be called before the threshold. It either only uses local properties or properties belonging to the type - never to the 'super' type (in the case of a derived class). It can't call any instance functions which aren't themselves selfless.
> 
> Example of use:
> class FooView : UIView
> {
>     var property : Int
> 
>     init()
>     {
>         initialiseProperty()
>         super.init()
>     }
> 
>     init(frame:CGRect)
>     {
>         initialiseProperty()
>         super.init(frame)
>     }
> 
>     selfless func initialiseProperty()
>     {
>         property = 4
>     }
> }
> 
> Is this something of interest?
> 
> Regards,
> Ross O'Brien
>  _______________________________________________
> swift-evolution mailing list
> swift-evolution at swift.org
> https://lists.swift.org/mailman/listinfo/swift-evolution

-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20160109/61082208/attachment.html>


More information about the swift-evolution mailing list