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

Tony Allevato tony.allevato at gmail.com
Mon Nov 6 02:53:53 CST 2017

On Sun, Nov 5, 2017 at 11: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.

Thanks for bringing this one back up!

> 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.

Naming the protocol CaseEnumerable/allCases seems like an unnecessary
restriction. There may be a complexity gap today in synthesizing the
requirement for types other than basic enums, but that doesn't mean that it
will always exist. What's the value of locking ourselves into the more
restrictive name? Protocols act as contracts that can be used for generic
programming and should thus be as generalized as possible. Any type could
implement ValueEnumerable and implement allValues by hand if they wanted.
If you don't intend to forbid this, then the name CaseEnumerable/allCases
is not an improvement over 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.

This goes back to the point above about generality in protocol design and
future-proofing—Sequence would be the most forward thinking way to specify
the allValues requirement, even if the synthesized version uses something
more refined, like Collection.

I would strongly prefer if the synthesized allValues for simple enums was
*not* an array, but rather a special Int-indexable RandomAccessCollection
(which could be wrapped in an AnyRandomAccessCollection to prevent leaking
implementation details in the API). The rationale for this is that the
information about the cases of an enum is already part of the static
metadata of a type, so we shouldn't force callers of allValues to incur a
heap allocation that is both (1) relatively slow and (2) moves information
we already have into a new copy in heap memory.

The idea that popped into my head for synthesizing the implementation is
something like this, where we map ordinals to cases. The optimizer will
compress this very nicely, I believe, so iterating the cases of an enum
would be extremely fast:

enum MyEnum: ValueEnumerable {
  case zero, one, two, three

  static var allValues: AnyRandomAccessCollection<MyEnum> {
    return AnyRandomAccessCollection((0..<4).lazy.map {
      switch $0 {
      case 0: return zero
      case 1: return one
      case 2: return two
      case 3: return three
      default: fatalError("unreachable")

Some might argue that the cases where this performance difference is a
concern are rare, but IMO an implementation synthesized by the compiler
should attempt to be as optimal as possible and not admit obvious
performance penalties if we can predict and avoid them.

There's also a possible optimization to the code above if you special case
RawRepresentable enums where all cases are in a contiguous range of
integral raw values—you can just have your `map` function call
init?(rawValue:) instead.

> ***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."*
> 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?

If we want to be consistent with Codable and Equatable/Hashable
conformance, then conformance should not be synthesized in an extension.
However, I personally want to remove that restriction for Eq/Hash so that
it can be added in same-file extensions, and Itai Ferber wants to do the
same for Codable (https://github.com/apple/swift/pull/11735).

In those cases, you would have a resilience problem if you could synthesize
in an extension outside the same file—Codable and Equatable/Hashable need
access to the private stored members of the type, so same-file extensions
are the only place where that would still work. For ValueEnumerable, you
don't have that problem, because cases are always public, so it could be
argued that synthesizing it anywhere is safe. So, in my mind, the only
issue becomes where we want to be consistent with the other synthesized
protocols or not.

> 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-dev/attachments/20171106/32d4f43e/attachment.html>

More information about the swift-dev mailing list