[swift-evolution] Re-pitch: Deriving collections of enum cases

Andrew Bennett cacoyi at gmail.com
Sat Nov 11 19:50:09 CST 2017


On Mon, Nov 6, 2017 at 6:54 PM, Jacob Bandes-Storch via swift-evolution <
swift-evolution at swift.org> wrote:

> Over a year ago, we discussed adding a magic "allValues"/"allCases" static
> property on enums with a compiler-derived implementation. The original proposal
> PR <https://github.com/apple/swift-evolution/pull/114> has been reopened
> for Swift 5 after languishing for a while, and I'd like to revisit it and
> make some changes before it goes up for formal review.
>
> Prior discussion: https://lists.swift.org/pipermail/swift-
> evolution/Week-of-Mon-20160411/015098.html (good luck finding the rest of
> the thread if you weren't on the list at the time...)
>
> [cc'd swift-dev for importer/availability-related topics below.]
>
> ***Naming***
>
> Given the complexity gap between a simple enumeration of cases and full
> support for non-enum types and associated values (which we don't intend to
> support with this proposal), I think it might be a good idea to adopt the
> names *CaseEnumerable/allCases* instead of ValueEnumerable/allValues.
>
> The original proposal didn't expose allValues as a requirement, for fear
> of unduly restricting its type. However, if the protocol's scope is more
> limited, *static var allCases* can be exposed as a requirement since the
> implementations are not likely to be complex. Furthermore...
>
>
> ***Generics***
>
> Since SE-0142
> <https://github.com/apple/swift-evolution/blob/master/proposals/0142-associated-types-constraints.md>
> was implemented in Swift 4, we now have more expressive options for the
> protocol requirements:
>
>   // 1 - array only
>   protocol CaseEnumerable {
>     static var allCases: [Self] { get }
>   }
>
>   // 2 - any sequence
>   protocol CaseEnumerable {
>     associatedtype *CaseSequence*: Sequence where CaseSequence.Element ==
> Self
>     static var *allCases*: CaseSequence { get }
>   }
>
>   // 3 - any collection
>   protocol CaseEnumerable {
>     associatedtype *CaseCollection*: Collection where
> CaseCollection.Element == Self
>     static var *allCases*: CaseCollection { get }
>   }
>
> This restricts the CaseEnumerable protocol to be used as a generic
> constraint, but that'd be true even with a plain array because of the Self
> type.
>
> Personally I like the flexibility provided by the associatedtype, but I
> also recognize it won't be incredibly useful for enums — more so if we
> wanted to provide e.g. UInt8.allValues, whose ideal implementation might be
> "return 0...UInt8.max". So I could see allowing allValues to be any
> sequence or collection, but flexibility for allCases might be less
> important. Others should weigh in here.
>
>
> ***Implementation strategy and edge cases***
>
> Last year <https://twitter.com/CodaFi_/status/920132464001024001>, Robert
> Widmann put together an implementation of CaseEnumerable: https://
> github.com/apple/swift/compare/master...CodaFi:ace-attorney
> I'd love to hear from anyone more familiar with the code whether there's
> anything we'd want to change about this approach.
>
> A few tricky situations have been brought to my attention:
>
> - Enums *imported from C/Obj-C* headers. Doug Gregor writes: *"The
> autogenerated allValues would only be able to list the enum cases it knows
> about from the header it was compiled with. If the library changes to
> add cases in the future (which, for example, Apple frameworks tend to do),
> those wouldn’t be captured in allValues."*
>
>
This proposal would be very useful in reducing boiler plate code in many
cases!

With "Enums imported from C/Obj-C headers", it would be nice to
automatically bridge it by using an appropriately namespaced (and
dynamically loaded) ObjC equivalent values(s), if one is available. I think
this addresses Doug's concerns.

For example:

//

// Foundation NSObjCRuntime.h

//

#define NS_VALUE_ENUMERABLE(__TYPE__) \

    FOUNDATION_EXPORT NSInteger const __TYPE__ ##ValueCount; \

    FOUNDATION_EXPORT __TYPE__ __TYPE__##ValueAtIndex(NSInteger index);


//

// UITableViewCellStyle.h

//


...


NS_VALUE_ENUMERABLE(UITableViewCellStyle);

//

// UITableViewCellStyle.m

//

...

NSInteger const      UITableViewCellStyleValueCount = 4;

UITableViewCellStyle UITableViewCellStyleValueAtIndex(NSInteger index) {

    // Can be a switch statement in more complicated cases

    return (UITableViewCellStyle) index;

}


//

// UITableViewCellStyle-Generated.swift

//


extension UITableViewCellStyle: ValueEnumerable {

    typealias AllValuesCollection = _NSValueEnumerableCollection<
UITableViewCellStyle>

    static var allValues: AllValuesCollection {

        return AllValuesCollection(

            count: UITableViewCellStyleValueCount,

            valueAtIndex: UITableViewCellStyleValueAtIndex)

    }

}

//

// Swift Standard Library

//

public struct _NSValueEnumerableCollection<T>: Collection {

    private let valueAtIndex: (Int) -> T

    public let count: Int

    public var startIndex: Int { return 0 }

    public var endIndex: Int { return count }


    public init(count: Int, valueAtIndex: @escaping (Int) -> T) {

        self.count = count

        self.valueAtIndex = valueAtIndex

    }


    public subscript(index: Int) -> T {

        // Potentially valueAtIndex could be @convention(c) returning

        // an integer type. T could be RawRepresentable, and conversion

        // errors could be handled here.

        return self.valueAtIndex(index)

    }


    public func index(after i: Int) -> Int {

        return i + 1

    }

}

Thanks,
Andrew Bennett

My understanding of the runtime/importer is very shallow, but with the
> current metadata-based strategy, I suspect imported enums couldn't be
> supported at all, or if they could, the metadata would be generated at
> import time rather than loaded dynamically from the library, which
> naturally wouldn't behave the same way when you drop in an upgraded version
> of the library. Is that correct?
>
> (Nonetheless, if a user really wanted this auto-generation, it would be
> nice to allow it somehow. Personally, I have had enums whose "source of
> truth" was an Obj-C header file, but since it was compiled in with the rest
> of the application, we didn't care at all about library upgrades. Maybe an
> internal extension adding a conformance can be allowed to participate in
> auto-generation?)
>
> - Enums with *availability* annotations on some cases. Doug Gregor
> writes: *"if I have a case that’s only available on macOS 10.12 and
> newer, it probably shouldn’t show up if I use allValues when running on
> macOS 10.11."*
>
> If we fetch cases from the enum metadata, does this "just work" since the
> metadata will be coming from whichever version of the library is loaded at
> runtime? If not, is it at least *possible* to extract availability info
> from the metadata? Finally, if not, should we try to synthesize an
> implementation that uses #available checks, or just refuse to synthesize
> allCases?
>
> - Should it be possible to add a CaseEnumerable conformance in an
> *extension*? My thinking is: we want to make sure the metadata is coming
> from the module that defines the enum, so we could restrict autogeneration
> of allCases to that same module. (That is, it wouldn't be possible to
> synthesize allCases for a CaseEnumerable extension on an enum from another
> module.) Although, it may be that I am missing something and this
> restriction isn't actually necessary. The question to answer is: in exactly
> which circumstances can the implementation be synthesized?
>
>
> Looking forward to hearing everyone's thoughts,
> Jacob
>
> _______________________________________________
> 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/20171112/9f61a044/attachment.html>


More information about the swift-evolution mailing list