[swift-evolution] Pitch: really_is and really_as operators

Charles Srstka cocoadev at charlessoft.com
Mon Aug 29 13:55:33 CDT 2016


> On Aug 29, 2016, at 11:14 AM, Joe Groff <jgroff at apple.com> wrote:
> 
>> 
>> On Aug 24, 2016, at 5:08 PM, Charles Srstka via swift-evolution <swift-evolution at swift.org> wrote:
>> 
>> MOTIVATION:
>> 
>> SE-0083 appears to be dead in the water, having been deferred until later in Swift 3 back in May and not having been heard from since then, with the Swift 3 release looming closer and closer. However, the predictability gains that would have been provided by this change remain desirable for cases where one needs to know the actual dynamic type of an entity before any bridging magic is involved. Additionally, performance-critical code may desire the ability to check something’s type quickly without incurring the overhead of Objective-C bridging code.
>> 
>> PROPOSED SOLUTION:
>> 
>> I propose the following operators: really_is, really_as, really_as?, and really_as!. These operators would only return a positive result if the type actually was what was being asked for, instead of something that might be able to bridge to that type.
>> 
>> DETAILED DESIGN:
>> 
>> let foo: Any = "Foo"
>> let bar: Any = NSString(string: "Bar")
>> 
>> let fooIsString = foo is String                  // true
>> let fooReallyIsString = foo really_is String     // true
>> 
>> let fooIsNSString = foo is NSString              // true
>> let fooReallyIsNSString = foo really_is NSString // false
>> 
>> let barIsString = bar is String                  // true
>> let barReallyIsString = bar really_is String     // false
>> 
>> let barIsNSString = bar is NSString              // true
>> let barReallyIsNSString = bar really_is NSString // true
>> 
>> ALTERNATIVES CONSIDERED:
>> 
>> Stick with using an unholy combination of Mirror and unsafeBitCast when you need to know what you’ve actually got.
> 
> It would be helpful to know why you want this. What are you trying to do?

Custom bridging behavior. My specific use case is obsolete since SE-0112, but before that I had my own little hacky SE-0112 implementation, that looked kind of like this:

protocol CSErrorType: ErrorType {
    var userInfo: [NSObject : AnyObject] { get }
    func toNSError() -> NSError
}

toNSError() had a default implementation that generated an appropriate NSError from the userInfo. This was pretty good, although I had to be diligent about always using .toNSError() rather than “as NSError”, so that the bridging behavior would be called. However, do/try/catch returns errors as ErrorType, which meant I had to do a dynamic check each time I caught an error, which was tedious. So, I decided to put an extension on ErrorType. The most convenient thing to do would be just to define a default implementation for toNSError() on ErrorType, but since there’s no way to make a method introduced by a protocol get dispatched dynamically, that would mean that CSErrorType’s implementation would never get called, even when it was appropriate. So I added a dynamic cast:

extension ErrorType {
    func toNSError() -> NSError {
        if let csError = self as? CSErrorType {
            return csError.toNSError()
        } else {
            return self as NSError
        }
    }
}

However, NSError conformed to ErrorType, and the code in “as NSError” apparently didn’t dynamically check the type of the error, so now if you called .toNSError() on a _SwiftNativeNSError that was statically typed as ErrorType, it would end up double-wrapped inside *another* _SwiftNativeNSError, which then prevented such errors from unwrapping back to Swift errors properly. So, my first thought was to just check for that:

extension ErrorType {
    func toNSError() -> NSError {
        if let ns = self as? NSError {
            return ns
        }

        if let csError = self as? CSErrorType {
            return csError.toNSError()
        } else {
            return self as NSError
        }
    }
}

But of course that will fail because “as? NSError” will always be true. Maybe “is” would be more honest?

extension ErrorType {
    func toNSError() -> NSError {
        if self is NSError {
            return unsafeBitCast(self, NSError.self)
        }

        if let csError = self as? CSErrorType {
            return csError.toNSError()
        } else {
            return self as NSError
        }
    }
}

Nope, of course “is NSError” is always true as well.

What I ended up doing was using a horrible little hack using Mirror that I’m really quite un-proud of to figure out if the thing was really an NSError or not. Knowing from this thread that “is” actually becomes honest when used on type(of:), I could have used that instead (yes, this example mixes Swift 2 and Swift 3, so I would have used “dynamicType” at the time; it’s just an example):

extension ErrorType {
    func toNSError() -> NSError {
        if type(of: self) is NSError.Type {
            return unsafeBitCast(self, NSError.self)
        }

        if let csError = self as? CSErrorType {
            return csError.toNSError()
        } else {
            return self as NSError
        }
    }
}

Unfortunately, the unsafeBitCast would probably have still been needed in order to avoid the bridging behavior when using “as!”.

While this particular use case isn’t really a “thing” anymore, someone else might have a different reason to bridge something in a custom way, and it’d be nice to have a way not to have to jump through all these hoops.

Charles



More information about the swift-evolution mailing list