Skip to content

Latest commit

 

History

History
357 lines (278 loc) · 11.4 KB

EnumStyle.rst

File metadata and controls

357 lines (278 loc) · 11.4 KB
orphan:

One of the issues that came up in our design discussions around Result was that enum cases don't really follow the conventions of anything else in our system. Our current convention for enum cases is to use CapitalizedCamelCase. This convention arose from the Cocoa NSEnumNameCaseName convention for constants, but the convention feels foreign even in the context of Objective-C. Non-enum type constants in Cocoa are often namespaced into classes, using class methods such as [UIColor redColor] (and would likely have been class properties if those were supported by ObjC). It's also worth noting that our "builtin" enum-like keywords such as true, false, and nil are lowercased, more like properties.

Swift also has enum cases with associated values, which don't have an immediate analog in Cocoa to draw inspiration from, but if anything feel "initializer-like". Aside from naming style, working with enum values also requires a different set of tools from other types, pattern matching with switch or if case instead of working with more readily-composable expressions. The compound effect of these style mismatches is that enums in the wild tend to grow a bunch of boilerplate helper members in order to make them fit better with other types. For example, Optional, aside from the massive amounts of language sugar it's given, vends initializers corresponding to Some and None cases:

extension Optional {
  init(_ value: Wrapped) {
    self = .Some(value)
  }

  init() {
    self = .None
  }
}

Result was proposed to have not only initializers corresponding to its Success and Error cases, but accessor properties as well:

extension Result {
  init(success: Wrapped) {
    self = .Success(success)
  }
  init(error: Error) {
    self = .Error(error)
  }

  var success: Wrapped? {
    switch self {
    case .Success(let success): return success
    case .Error: return nil
    }
  }
  var error: Error? {
    switch self {
    case .Success: return nil
    case .Error(let error): return error
    }
  }
}

This pattern of boilerplate also occurs in third-party frameworks that make heavy use of enums. Some examples from Github:

That people inside and outside of our team consider this boilerplate necessary for enums is a strong sign we should improve our core language design. I'd like to start discussion by proposing the following:

  • Because cases with associated values are initializer-like, declaring and using them ought to feel like using initializers on other types. A case declaration should be able to declare an initializer, which follows the same keyword naming rules as other initializers, for example:

    enum Result<Wrapped> {
      case init(success: Wrapped)
      case init(error: Error)
    }
    

    Constructing a value of the case can then be done with the usual initializer syntax:

    let success = Result(success: 1)
    let error = Result(error: SillyError.JazzHands)
    

    And case initializers can be pattern-matched using initializer-like matching syntax:

    switch result {
    case Result(success: let success):
      ...
    case Result(error: let error):
      ...
    }
    
  • Enums with associated values implicitly receive internal properties corresponding to the argument labels of those associated values. The properties are optional-typed unless a value with the same name and type appears in every case. For example, this enum:

    public enum Example {
      case init(foo: Int, alwaysPresent: String)
      case init(bar: Int, alwaysPresent: String)
    }
    

    receives the following implicit members:

    /*implicit*/
    internal extension Example {
      var foo: Int? { get }
      var bar: Int? { get }
      var alwaysPresent: String { get } // Not optional
    }
    
  • Because cases without associated values are property-like, they ought to follow the lowercaseCamelCase naming convention of other properties. For example:

    enum ComparisonResult {
      case descending, same, ascending
    }
    
    enum Bool {
      case true, false
    }
    
    enum Optional<Wrapped> {
      case nil
      case init(_ some: Wrapped)
    }
    

Since this proposal affects how we name things, it has ABI stability implications (albeit ones we could hack our way around with enough symbol aliasing), so I think we should consider this now. It also meshes with other naming convention discussions that have been happening.

I'll discuss the points above in more detail:

Case Initializers

Our standard recommended style for cases with associated values should be to declare them as initializers with keyword arguments, much as we do other kinds of initializer:

enum Result<Wrapped> {
  case init(success: Wrapped)
  case init(error: Error)
}

enum List<Element> {
  case empty
  indirect case init(element: Element, rest: List<Element>)
}

It should be possible to declare unlabeled case initializers too, for types like Optional with a natural "primary" case:

enum Optional<Wrapped> {
  case nil
  case init(_ some: Wrapped)
}

Patterns should also be able to match against case initializers:

switch result {
case Result(success: let s):
  ...
case Result(error: let e):
  ...
}

Overloading

I think it would also be reasonable to allow overloading of case initializers, as long as the associated value types cannot overlap. (If the keyword labels are overloaded and the associated value types overlap, there would be no way to distinguish the cases.) Overloading is not essential, though, and it would be simpler to disallow it.

Named cases with associated values

One question would be, if we allow case init declarations, whether we should also remove the existing ability to declare named cases with associated values:

enum Foo {
  // OK
  case init(foo: Int)
  // Should this become an error?
  case foo(Int)
}

Doing so would help unambiguously push the new style, but would drive a syntactic wedge between associated-value and no-associated-value cases. If we keep named cases with associated values, I think we should consider altering the declaration syntax to require keyword labels (or explicit _ to suppress labels), for better consistency with other function-like decls:

enum Foo {
  // Should be a syntax error, 'label:' expected
  case foo(Int)

  // OK
  case foo(_: Int)

  // OK
  case foo(label: Int)
}

Shorthand for init-style cases

Unlike enum cases and static methods, initializers currently don't have any contextual shorthand when the type of an initialization can be inferred from context. This could be seen as an expressivity regression in some cases. With named cases, one can write:

foo(.Left(x))

but with case initializers, they have to write:

foo(Either(left: x))

Some would argue this is clearer. It's a bit more painful in switch patterns, though, where the type would need to be repeated redundantly:

switch x {
case Either(left: let left):
  ...
case Either(right: let right):
  ...
}

One possibility would be to allow .init, like we do other static methods:

switch x {
case .init(left: let left):
  ...
case .init(right: let right):
  ...
}

Or maybe allow labeled tuple patterns to match, leaving the name off altogether:

switch x {
case (left: let left):
  ...
case (right: let right):
  ...
}

Implicit Case Properties

The only native operation enums currently support is switch-ing. This is nice and type-safe, but switch is heavyweight and not very expressive. We now have a large set of language features and library operators for working with Optional, so it is expressive and convenient in many cases to be able to project associated values from enums as Optional values. As noted above, third-party developers using enums often write out the boilerplate to do this. We should automate it. For every case init with labeled associated values, we can generate an internal property to access that associated value. The value will be Optional, unless every case has the same associated value, in which case it can be nonoptional. To repeat the above example, this enum:

public enum Example {
  case init(foo: Int, alwaysPresent: String)
  case init(bar: Int, alwaysPresent: String)
}

receives the following implicit members:

/*implicit*/
internal extension Example {
  var foo: Int? { get }
  var bar: Int? { get }
  var alwaysPresent: String { get } // Not optional
}

Similar to the elementwise initializer for struct types, these property accessors should be internal, since they rely on potentially fragile layout characteristics of the enum. (Like the struct elementwise initializer, we ought to have a way to easily export these properties as public when desired too, but that can be designed separately.)

These implicit properties should be read-only, until we design a model for enum mutation-by-part.

An associated value property should be suppressed if:

  • there's an explicit declaration in the type with the same name:

    enum Foo {
      case init(foo: Int)
    
      var foo: String { return "foo" } // suppresses implicit "foo" property
    }
    
  • there are associated values with the same label but conflicting types:

    enum Foo {
      case init(foo: Int, bar: Int)
      case init(foo: String, bas: Int)
    
      // No 'foo' property, because of conflicting associated values
    }
    
  • if the associated value has no label:

    enum Foo {
      case init(_: Int)
    
      // No property for the associated value
    }
    

    An associated value could be unlabeled but still provide an internal argument name to name its property:

    enum Foo {
      case init(_ x: Int)
      case init(_ y: String)
    
      // var x: Int?
      // var y: String?
    }
    

Naming Conventions for Enum Cases

To normalize enums and bring them into the "grand unified theory" of type interfaces shared by other Swift types, I think we should encourage the following conventions:

  • Cases with associated values should be declared as case init initializers with labeled associated values.
  • Simple cases without associated values should be named like properties, using lowercaseCamelCase. We should also import Cocoa NS_ENUM and NS_OPTIONS constants using lowercaseCamelCase.

This is a big change from the status quo, including the Cocoa tradition for C enum constants, but I think it's the right thing to do. Cocoa uses the NSEnumNameCaseName convention largely because enum constants are not namespaced in Objective-C. When Cocoa associates constants with class types, it uses its normal method naming conventions, as in UIColor.redColor. In Swift's standard library, type constants for structs follow the same convention, for example Int.max and Int.min. The literal keywords true, false, and nil are arguably enum-case-like and also lowercased. Simple enum cases are essentially static constant properties of their type, so they should follow the same conventions.