I'm trying to implement a JSON-like (BSON) interface wherein querying a document for a given key returns nil when the key does not exist and NSNull when the key exists, but its associated value is null.
To that end, we define a protocol BSONValue which has a method for getting the various types that the underlying document supports. It has the signature static func from(...) -> Self.
e.g. the corresponding implementation for the Bool type, which is supported by JSON/BSON, is public static func from(...) -> Bool { //read from document }.
As mentioned, we would like NSNull to conform to BSONValue for the cases in which a document looks like {"a": null}. On MacOS, the implementation of the from method for NSNull looks like: public static func from(...) -> Self { return self.init() }
Which works fine.
However, as the title suggests, this does not work on Linux, specifically under versions of Swift 4.0 and 4.2 (not 4.1?). It appears that in the linux version of Foundation, NSNull.init() is not a required initializer, so we cannot call it via self in the context of the from method.
The exact error is the following:
error: constructing an object of class type 'Self' with a metatype value must use a 'required' initializer`
Foundation.NSNull:4:12: note: selected non-required initializer 'init()'
public init()
Is this inconsistency a bug in Foundation?
(I'm also not sure if this is the best way/only way to do this, so if you all have any suggestions for alternative implementations, I would be happy to hear them as well.)
I bet this is a difference between the fact that NSNull.init() bridges from +[NSNull null] on Darwin, and factory initializers that bridge this way in Objective-C are subject to slightly different rules than initializers in pure Swift; NSNull.init() is a real Swift initializer in swift-corelibs-foundation.
So, the problem here is the Self requirement. I'll write my understanding of it, and the reader is encouraged jump in to correct if I get anything wrong, please.
The way Swift initializes, it requires you to invoke an initializer that exists on the class that you are using. But attention must be given to Self, as that that method can be invoked on a subclass. At that point, the compiler requires you to guarantee that the init() initializer exists not just on NSNull, but on all possible sold bclasses.
There are a number of ways to guarantee that that's the case. One is to guarantee that your class can't have subclasses — your code would work if NSNull was final. (An aside: since NSNull is an ObjC class, it can only import as an open class, and so we have to match it that way too in swift-corelibs-foundation. So you can't really change the fact that it may be subclassed.)
It can also work if you require your subclasses to have init(). You do so by marking the initializer as required. All subclasses will then be required to have it (overriding it). This isn't the case with NSNull either, though. (In ObjC, +null is declared to be instancetype aka Self, and Swift trusts the implementation on this and knows that that method is inherited by ObjC subclasses, I assume.)
Instead of trying to shoehorn this in Swift's initializer model, may I interest you in Codable? That's the canonical way to traverse a tree to create a serialized representation or create an object tree from a serialized format in Swift now. BSON is rather similar to JSON in many regards, and it would be amazing if you could make BSONEncoder/Decoder instead. Codable's compositional approach neatly fits in the model without having to face this kind of issue.
@jrose If the objc_subclassing_restricted attribute was added to NSNull, could the clang importer use final class NSNull (instead of open class NSNull)?
Yes; but that's a breaking API change that we may or may not be willing to make. (I can't comment on what may or may not happen on the ObjC side of things.)
We have considered extending NSNull ourselves and either making it final or marking init() as required, but we'd rather not introduce a new type for this if we can avoid it.
Some of the other commenters have mentioned possible changes on the Swift/Foundation side that would solve this--could such changes be possibly considered for 5.0?
Also, some of the implementation details were left out from my original post--we've already added encoders and decoders for BSON. The current issue is largely unrelated to that.