[swift-evolution] Pitch: Import Objective-C Constants as Enums

Jeff Kelley slaunchaman at gmail.com
Wed Jan 20 20:39:25 CST 2016


> On Jan 20, 2016, at 7:41 PM, Douglas Gregor <dgregor at apple.com> wrote:
> 
> 
>> On Jan 20, 2016, at 2:11 PM, Jordan Rose <jordan_rose at apple.com <mailto:jordan_rose at apple.com>> wrote:
>> 
>> 
>>> On Jan 17, 2016, at 22:10 , Douglas Gregor via swift-evolution <swift-evolution at swift.org <mailto:swift-evolution at swift.org>> wrote:
>>> 
>>>> 
>>>> On Jan 17, 2016, at 7:13 PM, Jeff Kelley via swift-evolution <swift-evolution at swift.org <mailto:swift-evolution at swift.org>> wrote:
>>>> 
>>>> A lot of Cocoa APIs have long lists of constant values, typically NSStrings. I’d like to pitch a way to import them as enums with associated types. I can write up a full proposal if people think this is a good idea, but here’s my thinking:
>>>> 
>>>> Let’s take the error domains in NSError.h for a quick example. These entries in the header:
>>>> 
>>>>> FOUNDATION_EXPORT NSString *const NSCocoaErrorDomain;
>>>>> FOUNDATION_EXPORT NSString *const NSPOSIXErrorDomain;
>>>>> FOUNDATION_EXPORT NSString *const NSOSStatusErrorDomain;
>>>>> FOUNDATION_EXPORT NSString *const NSMachErrorDomain;
>>>> 
>>>> 
>>>> turn into this in the Swift interface:
>>>> 
>>>>> public let NSCocoaErrorDomain: String
>>>>> public let NSPOSIXErrorDomain: String
>>>>> public let NSOSStatusErrorDomain: String
>>>>> public let NSMachErrorDomain: String
>>>> 
>>>> What I’m proposing is a way to import those as an enum instead. Similar to how we mark sections of Objective-C code with NS_ASSUME_NONNULL_BEGIN, we could mark it with something like NS_CASE_LIST_BEGIN. Then, this code:
>>>> 
>>>>> NS_CASE_LIST_BEGIN;
>>>>> 
>>>>> FOUNDATION_EXPORT NSString *const NSCocoaErrorDomain;
>>>>> FOUNDATION_EXPORT NSString *const NSPOSIXErrorDomain;
>>>>> FOUNDATION_EXPORT NSString *const NSOSStatusErrorDomain;
>>>>> FOUNDATION_EXPORT NSString *const NSMachErrorDomain;
>>>>> 
>>>>> NS_CASE_LIST_END;
>>>> 
>>>> would be imported as follows:
>>>> 
>>>>> enum ErrorDomain : String {
>>>>>     case Cocoa
>>>>>     case POSIX
>>>>>     case OSStatus
>>>>>     case Mach
>>>>> }
>>>> 
>>>> I can think of a lot of areas in Cocoa where these APIs could make things much more type-safe in Swift. Is this a good idea? Would people use this?
>>> 
>>> FWIW, this has come up a number of times in discussions among Swift developers (although not, IIRC, on swift-evolution). Our current favored way to write this in (Objective-)C would be with a new typedef of NSString * that has some special attribute on it, e.g.,
>>> 
>>>   typedef NSString * NSErrorDomain __attribute__((enum(string)));
>>> 
>>>   FOUNDATION_EXPORT NSErrorDomain const NSCocoaErrorDomain;
>>>   FOUNDATION_EXPORT NSErrorDomain const NSPOSIXErrorDomain;
>>>   FOUNDATION_EXPORT NSErrorDomain const NSOSStatusErrorDomain;
>>>   FOUNDATION_EXPORT NSErrorDomain const NSMachErrorDomain;
>>> 
>>> The typedef would import as a String-backed enum and all of the string constants declared with that typedef within the same module as the typedef would become cases of that enum. String constants declared with that typedef in a *different* module would become “static lets” within extensions of the String-backed enum.
>>> 
>>> Call that a +1 from me on your idea :)
>> 
>> I still have reservations about this:
>> 
>> - Most of these strings are not things you switch on, making the enum-ness not particularly interesting. I'd be happier with a RawRepresentable struct.
> 
> I think a RawRepresentable struct captures the intended semantics well. Jeff, what do you think?

I haven’t used RawRepresentable by itself before. Is this the kind of result we’d get?

struct NSErrorDomain: RawRepresentable {
    
    static let Cocoa: MyRelatedValue
    static let POSIX: MyRelatedValue
    static let OSStatus: MyRelatedValue
    static let Mach: MyRelatedValue
    
    typealias RawValue = String
    
    init?(rawValue: RawValue)
    
    var rawValue: String  { get }

}

If so, I think that has the same benefits of an enum without the added baggage. We get a type for the values, a list of the public instances of the value, and the typedef in Objective-C even makes that language more readable. As long as we can still get to an instance of the value using the short dot syntax:

	let foo: NSErrorDomain = .Cocoa

Then I see no downside to a struct vs. an enum. The only reason I’d thought of an enum in the first place was that I’ve seen existing Swift code use enums to group related strings—user defaults keys, etc.


>> - Our current prefix-stripping logic relies on being able to see all the cases. You can do this with an enum because they're all declared in one block, but string constants they may cross multiple files. This isn't impossible to deal with, but I think it's counterintuitive. (An alternative would be to only use the type name for stripping each value individually.)
> 
> Our two options are to compare against just the type name or to compare against just the set of cases that come from the module that defines the typedef… except the latter might not be unique because typedefs can be redeclared in Objective-C and C++. I think that says we should just compare against the type name.

Using the type name for stripping seems acceptable to me.

>> - I'm not sure where the line is between "an open set of choices represented as strings" and "strings with some defaults defined". (Very few of these are truly closed sets; if we add a new UIFont attribute next year, it should prefix-strip like everything else.)
> 
> Perhaps the line is a somewhat squishy “it is rare or impossible for a client of the API to define new values”?

I agree. NSErrorDomain wasn’t a great example because you can define your own. My pull request used HealthKit identifiers, which are a better example as the user of the API can’t define their own.

Adding a new value in the future should definitely be as straightforward as possible, but I’m not sure what about this approach makes that more difficult.

>> - As a nitpick, I think a typedef for 'NSString' rather than 'NSString *' would be preferred, so that the '*' still shows up in the declaration in Objective-C. This is just a feeling though, and maybe it's just clinging to the way things are done now.’
> 
> WFM.

Yep, that seems reasonable. Thanks for your feedback, Jordan and Doug!
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20160120/1a63033a/attachment.html>


More information about the swift-evolution mailing list