Permalink
280 lines (200 sloc) 14 KB

Derived Collection of Enum Cases

Introduction

It is a truth universally acknowledged, that a programmer in possession of an enum with many cases, must eventually be in want of dynamic enumeration over them.

Enumeration types without associated values (henceforth referred to as "simple enums") have a finite, fixed number of cases, yet working with them programmatically is challenging. It would be natural to enumerate all the cases, count them, determine the highest rawValue, or produce a Collection of them. However, despite the fact that both the Swift compiler and the Swift runtime are aware of this information, there is no safe and sanctioned way for users to retrieve it. Users must resort to various workarounds in order to iterate over all cases of a simple enum.

This topic was brought up three different times in just the first two months of swift-evolution's existence. It was the very first language feature request on the Swift bug tracker. It's a frequent question on Stack Overflow (between them, these two questions have over 400 upvotes and 60 answers). It's a popular topic on blogs. It is one of just eight examples shipped with Sourcery.

We propose the introduction of a protocol, CaseIterable, to indicate that a type has a finite, enumerable set of values. Moreover, we propose an opt-in derived implementation of CaseIterable for the common case of a simple enum.

Prior discussion on Swift-Evolution

…more than a year passes…

= a precursor to this proposal

Motivation

Use cases

Examples online typically pose the question "How do I get all the enum cases?", or even "How do I get the count of enum cases?", without fully explaining what the code will do with that information. To guide our design, we focus on two categories of use cases:

  1. The code must greedily iterate over all possible cases, carrying out some action for each case. For example, imagine enumerating all combinations of suit and rank to build a deck of playing cards:

    let deck = Suit.magicListOfAllCases.flatMap { suit in
        (1...13).map { rank in
            PlayingCard(suit: suit, rank: rank)
        }
    }
  2. The code must access information about all possible cases on demand. For example, imagine displaying all the cases through a lazy rendering mechanism like UITableViewDataSource:

    class SuitTableViewDataSource: NSObject, UITableViewDataSource {
        func tableView(_: UITableView, numberOfRowsInSection section: Int) -> Int {
            return Suit.magicListOfAllCases.count
        }
    
        func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
            let suit = Suit.magicListOfAllCases[indexPath.row]
            cell.titleView!.text = suit.localizedName
            return cell
        }
    }

To limit our scope, we are primarily interested in simple enums—those without any associated values—although we would also like to allow more complicated enums and structs to manually participate in this mechanism.

The second use case suggests that access by contiguous, zero-based integer index is important for at least some uses. At minimum, it should be easy to construct an Array from the list of cases, but ideally the list could be used like an array directly, at least for simple enums.

Workarounds

The most basic approach to producing a collection of all cases is by manual construction:

enum Attribute {
  case date, name, author
}
protocol Entity {
  func value(for attribute: Attribute) -> Value
}
// Cases must be listed explicitly:
[Attribute.date, .name, .author].map{ entity.value(for: $0) }.joined(separator: "\n")

For RawRepresentable enums, users have often relied on iterating over the known (or assumed) allowable raw values:

Excerpt from Nate Cook's post, Loopy, Random Ideas for Extending "enum" (October 2014):

enum Reindeer: Int {
    case Dasher, Dancer, Prancer, Vixen, Comet, Cupid, Donner, Blitzen, Rudolph
}
extension Reindeer {
    static var allCases: [Reindeer] {
        var cur = 0
        return Array(
            GeneratorOf<Reindeer> {
                return Reindeer(rawValue: cur++)
            }
        )
    }
    static var caseCount: Int {
        var max: Int = 0
        while let _ = self(rawValue: ++max) {}
        return max
    }
    static func randomCase() -> Reindeer {
        // everybody do the Int/UInt32 shuffle!
        let randomValue = Int(arc4random_uniform(UInt32(caseCount)))
        return self(rawValue: randomValue)!
    }
}

Or creating the enums by unsafeBitCast from their hashValue, which is assumed to expose their memory representation:

Excerpt from Erica Sadun's post, Swift: Enumerations or how to annoy Tom, with full implementation in this gist (July 12, 2015):

static func fromHash(hashValue index: Int) -> Self {
    let member = unsafeBitCast(UInt8(index), Self.self)
    return member
}

public init?(hashValue hash: Int) {
    if hash >= Self.countMembers() {return nil}
    self = Self.fromHash(hashValue: hash)
}

Or using a switch statement, making it a compilation error to forget to add a case, as with Dave Sweeris's Enum Enhancer (which includes some extra functionality to avoid the boilerplate required for .cases and .labels):

enum RawValueEnum : Int, EnumeratesCasesAndLabels {
    static let enhancer:EnumEnhancer<RawValueEnum> = EnhancedGenerator {
        // `$0` is a RawValueEnum?
        switch $0 {
        case .none: $0 = .zero
        case .some(let theCase):
            switch theCase {
            case .zero: $0 = .one
            case .one: $0 = nil
            }
        }
    }
    case zero = 0
    case one = 1
}

There are many problems with these existing techniques:

  • They are ad-hoc and can't benefit every enum type without duplicated code.
  • They are not standardized across codebases, nor provided automatically by libraries such as Foundation and {App,UI}Kit.
  • They are dangerous at worst, bug-prone in most cases (such as when enum cases are added, but the user forgets to update a hard-coded static collection), and awkward at best.

Resilience implications

This last point is especially important as we begin to concern ourselves with library resilience. Future versions of Swift should allow library authors to add and deprecate public enum cases without breaking binary compatibility. But if clients are manually constructing arrays of "all cases", those arrays will not correspond to the version of the library they are running against.

At the same time, the principle that libraries ought to control the promises they make should apply to any case-listing feature. Participation in a "list all cases" mechanism should be optional and opt-in.

Precedent in other languages

  • Rust does not seem to have a solution for this problem.

  • C#'s Enum has several methods available for reflection, including GetValues() and GetNames().

  • Java implicitly declares a static values() function, returning an array of enum values, and valueOf(String name) which takes a String and returns the enum value with the corresponding name (or throws an exception). More examples here.

  • The Template Haskell extension to Haskell provides a function reify which extracts info about types, including their constructors.

Proposed solution

We propose introducing a CaseIterable protocol to the Swift Standard Library. The compiler will derive an implementation automatically for simple enums when the conformance is specified.

enum Ma: CaseIterable { case , , , , , , 🐎, 🐴 }

Ma.allCases         // returns some Collection whose Iterator.Element is Ma
Ma.allCases.count   // returns 8
Array(Ma.allCases)  // returns [Ma.马, .吗, .妈, .码, .骂, .麻, .🐎, .🐴]

Detailed design

  • The CaseIterable protocol will have the following declaration:

    public protocol CaseIterable {
      associatedtype AllCases: Collection where AllCases.Element == Self
      static var allCases: AllCases { get }
    }
  • The compiler will synthesize an implementation of CaseIterable for an enum type if and only if:

    • the enum contains only cases without associated values;
    • the enum declaration has an explicit CaseIterable conformance (and does not fulfill the protocol's requirements).
  • Enums imported from C/Obj-C headers will not participate in the derived CaseIterable conformance.

  • Cases marked unavailable will not be included in allCases.

  • The implementation will not be synthesized if the conformance is on an extension — it must be on the original enum declaration.

Source compatibility

This proposal only adds functionality, so existing code will not be affected. (The identifier CaseIterable doesn't make very many appearances in Google and GitHub searches.)

Effect on ABI stability

The proposed implementation adds a derived conformance that makes use of no special ABI or runtime features.

Effect on API resilience

User programs will come to rely on the CaseIterable protocol and its allCases and AllCases requirements. Due to the use of an associated type for the property's type, the derived implementation is free to change the concrete Collection it returns without breaking the API.

Alternatives considered

The functionality could also be provided entirely through the Mirror/reflection APIs. This would likely result in much more obscure and confusing usage patterns.

Provide a default collection

Declaring this in the Standard Library reduces the amount of compiler magic required to implement the protocol. However, it also exposes a public unsafe entrypoint to the reflection API that we consider unacceptable.

extension CaseIterable where AllCases == DefaultCaseCollection<Self> {
    public static var allCases: DefaultCaseCollection<Self> {
        return DefaultCaseCollection(unsafeForEnum: Self.self)
    }
}

public struct DefaultCaseCollection<Enum>: RandomAccessCollection {
    public var startIndex: Int { return 0 }
    public let endIndex: Int

    public init(unsafeForEnum _: Enum.Type) {
        endIndex = _countCaseValues(Enum.self)
    }

    public subscript(i: Int) -> Enum {
        precondition(indices.contains(i), "Case index out of range")
        return Builtin.reinterpretCast(i) as Enum
    }
}

Metatype conformance to Collection

A type inherently represents a set of its possible values, and as such, for val in MyEnum.self { } could be a natural way to express iteration over all cases of a type. Specifically, if the metatype MyEnum.Type could conform to Collection or Sequence (with Element == MyEnum), this would allow for-loop enumeration, Array(MyEnum.self), and other use cases. In a generic context, the constraint T: CaseIterable could be expressed instead as T.Type: Collection.

Absent the ability for a metatype to actually conform to a protocol, the compiler could be taught to treat the special case of enum types as if they conformed to Collection, enabling this syntax before metatype conformance became a fully functional feature.