[swift-evolution] [Proposal] Enums with static stored propertiesfor each case

Leonardo Pessoa me at lmpessoa.com
Tue May 31 11:16:56 CDT 2016


Just complementing as I was on the run when I saw and responded the
previous message.

In my proposal, we would be allowing tuples to be used as if they were
value types in enums.

|   enum Planet : (mass: Double, radius: Double) {
|       case Mercury = (mass: 3.303e+23, radius: 2.4397e6)
|       case Venus = (mass: 4.869e+24, radius: 6.0518e6)
|       case Earth = (mass: 5.976e+24, radius: 6.37814e6)
|       case Mars = (mass: 6.421e+23, radius: 3.3972e6)
|       case Jupiter = (mass: 1.9e+27, radius: 7.1492e7)
|       case Saturn = (mass: 5.688e+26, radius: 6.0268e7)
|       case Uranus = (mass: 8.686e+25, radius: 2.5559e7)
|       case Neptune = (mass: 1.024e+26, radius: 2.4746e7)
|   }

Since tuples cannot be tested for uniqueness (I'd even go further to
say this would be undesireable here), you would also not have the
init(rawValue:) method testing for tuples and thus you would not have
the rawValue property of single typed enums returning the tuple
either, just like non-typed enums (calling .rawValue on a non-typed
enum will return the enum value itself, thus calling
Planet.Mercury.rawValue would return .Mercury itself here too). This
imposes no changes on existing code and allows for the values to be
refered to directly without using .rawValue in the middle (e.g.
print(Planet.Mercury.radius) instead of
print(Planet.Mercury.rawValue.radius)). You could even think of
internally representing single typed enums using a single value tuple
like the following:

|   enum Order : Int {
|       case First = 1
|       case Second
|       case Third
|       case Fourth
|   }

could be still declared like this but be internally represented as if it were:

|   enum Order : (rawValue: Int) {
|       case First = (rawValue: 1)
|       case Second = (rawValue: 2)
|       case Third = (rawValue: 3)
|       case Fourth = (rawValue: 4)
|   }

But that's just a simplification for internal representation and may
not even be used (I think this will be left for the Apple team to
decide how it should be implemented, either way current raw values
won't be broken).

IMO, this proposal only expands the possible set of values a typed
enum can hold using a syntax that is as close to the current one as I
think possible. Enums are, as defined in the Swift documentation, "a
common type for a group of related **values**" afterall and it makes
no sense to me to associate computed properties (which could return
different values each time they're called) to store constant value
properties and it's also much more verbose. Tuples can do this job
much better, cleaner and efficiently. You may also remember enums with
associated values: their values are defined when the enum is
initialised and never change either; you may create a new instance
with new values derived from another instance but you can never change
the values of an instance. Thus allowing an enum value to hold a
computed property seem like we're changing enums into something else.

Just one more idea to bring to this proposal, I thought there could be
a way to still have the init(rawValue:) method using tuples by simply
declaring one of the values of the tuple to be rawValue itself. The
compiler would enforce the uniqueness of values in this field and the
initialiser could find the enum value by its raw value, like this:

|   enum Planet : (mass: Double, radius: Double, rawValue: Int) {
|      case Mercury = (mass: 3.303e+23, radius: 2.4397e6, rawValue: 1)
|      case Venus = (mass: 4.869e+24, radius: 6.0518e6, rawValue: 2)
|      case Earth = (mass: 5.976e+24, radius: 6.37814e6, rawValue: 3)
|      case Mars = (mass: 6.421e+23, radius: 3.3972e6, rawValue: 4)
|      case Jupiter = (mass: 1.9e+27, radius: 7.1492e7, rawValue: 5)
|      case Saturn = (mass: 5.688e+26, radius: 6.0268e7, rawValue: 6)
|      case Uranus = (mass: 8.686e+25, radius: 2.5559e7, rawValue: 7)
|      case Neptune = (mass: 1.024e+26, radius: 2.4746e7, rawValue: 8)
|   }

and thus allow the following code:

|   let planet = Planet(rawValue: 4)!
|   print(planet.mass)

You may argue this is hard to read the more fields the tuple holds,
but there is nothing preventing you from formating your code in
another way, for example:

|   enum Planet : (mass: Double, radius: Double, rawValue: Int) {
|      case Mercury = (
|          mass: 3.303e+23,
|          radius: 2.4397e6,
|          rawValue: 1
|      )
|      case Venus = (
|          mass: 4.869e+24,
|          radius: 6.0518e6,
|          rawValue: 2
|      )
|      // ....

That was my proposal for the problem you presented.

On 31 May 2016 at 11:23, Leonardo Pessoa <me at lmpessoa.com> wrote:
> As I said before, I'm not in favour of this approach. And you completely
> missed my proposal in the alternatives.
>
> ________________________________
> From: Jānis Kiršteins
> Sent: ‎31/‎05/‎2016 11:17 AM
> To: Leonardo Pessoa
> Cc: Brent Royal-Gordon; swift-evolution
> Subject: Re: [swift-evolution] [Proposal] Enums with static stored
> propertiesfor each case
>
> I wrote a proposal draft:
>
> # Enum case stored properties
>
> * Proposal: TBD
> * Author: [Janis Kirsteins](https://github.com/kirsteins)
> * Status: TBD
> * Review manager: TBD
>
> ## Introduction
>
> This proposal allows each enum case to have stored properties.
>
> ## Motivation
>
> Enums cases can have a lot of constant (or variable) static values
> associated with it. For example, planets can have mass, radius, age,
> closest star etc. Currently there is no way to set or get those values
> easily.
>
> Example below shows that is hard to read and manage static associated
> values with each case. It is hard to add or remove case as it would
> require to add or remove code in four different places in file. Also
> static associated value like `UIBezierPath` is recreated each time the
> property is computed while it's constant.
>
> ```swift
> enum Suit {
>     case spades
>     case hearts
>     case diamonds
>     case clubs
>
>     var simpleDescription: String {
>         switch self {
>         case .spades:
>             return "spades"
>         case .hearts:
>             return "hearts"
>         case .diamonds:
>             return "diamonds"
>         case .clubs:
>             return "clubs"
>         }
>     }
>
>     var color: UIColor {
>         switch self {
>         case .spades:
>             return .blackColor()
>         case .hearts:
>             return .redColor()
>         case .diamonds:
>             return .redColor()
>         case .clubs:
>             return .blackColor()
>         }
>     }
>
>     var symbol: String {
>         switch self {
>         case .spades:
>             return "♠"
>         case .hearts:
>             return "♥"
>         case .diamonds:
>             return "♦"
>         case .clubs:
>             return "♣"
>         }
>     }
>
>     var bezierPath: UIBezierPath {
>         switch self {
>         case .spades:
>             let path = UIBezierPath()
>             // omitted lines ...
>             return path
>         case .hearts:
>             let path = UIBezierPath()
>             // omitted lines ...
>             return path
>         case .diamonds:
>             let path = UIBezierPath()
>             // omitted lines ...
>             return path
>         case .clubs:
>             let path = UIBezierPath()
>             // omitted lines ...
>             return path
>         }
>     }
> }
> ```
>
> ## Proposed solution
>
> Support stored properties for enum cases just as each case were an
> instance. Case properties are initialized block after each case
> declaration.
>
> ```swift
> enum Suit {
>     let simpleDescription: String
>     let color: UIColor
>     let symbol: String
>     let bezierPath: UIBezierPath
>
>     case spades {
>         simpleDescription = "spades"
>         color = .blackColor()
>         symbol = "♠"
>         let bezierPath = UIBezierPath()
>         // omitted lines ...
>         self.bezierPath = bezierPath
>     }
>
>     case hearts {
>         simpleDescription = "hearts"
>         color = .redColor()
>         symbol = "♥"
>         let bezierPath = UIBezierPath()
>         // omitted lines ...
>         self.bezierPath = bezierPath
>     }
>
>     case diamonds {
>         simpleDescription = "diamonds"
>         color = .redColor()
>         symbol = "♦"
>         let bezierPath = UIBezierPath()
>         // omitted lines ...
>         self.bezierPath = bezierPath
>     }
>
>     case clubs {
>         simpleDescription = "clubs"
>         color = .blackColor()
>         symbol = "♣"
>         let bezierPath = UIBezierPath()
>         // omitted lines ...
>         self.bezierPath = bezierPath
>     }
> }
>
> let symbol = Suit.spades.symbol // "♠"
> ```
>
> The proposed solution improves:
> - Readability as cases are closer with their related data;
> - Improves code maintainability as a case can be removed or added in one
> place;
> - Improved performance as there is no need to recreate static values;
> - ~30% less lines of code in given example.
>
> ## Detailed design
>
> #### Stored properties
>
> Enum stored properties are supported the same way they are supported
> for structs can classes. Unlike enum associated values, stored
> properties are static to case and are shared for the same case.
>
> Properties are accessed:
> ```swift
> let simpleDescription = Suit.spades.simpleDescription
> ```
>
> Mutable properties can be set:
> ```swift
> Suit.spades.simpleDescription = "new simple description"
> ```
>
> #### Initialization
>
> If enum has uninitialized stored property it must be initialized in a
> block after each case declaration. The block work the same way as
> struct initialization. At the end of initialization block all
> properties must be initialized.
>
> ```swift
> enum Suit {
>     var simpleDescription: String
>
>     case spades {
>         simpleDescription = "spades"
>     }
> }
> ```
>
> Initialization block can be combine with use of `rawValue`:
>
> ```swift
> enum Suit: Int {
>     var simpleDescription: String
>
>     case spades = 1 {
>         simpleDescription = "spades"
>     }
> }
> ```
> or associated values of the case:
>
> ```swift
> enum Suit {
>     var simpleDescription: String
>
>     case spades(Int) {
>         simpleDescription = "spades"
>     }
> }
> ```
>
> ## Impact on existing code
>
> Stored properties for enums are not currently not supported, so there
> is no impact on existing code.
>
> ## Alternatives considered
>
> - Use labeled tuple as `rawValue` of the enum case. This approach is
> not compatible as it conflicts with intention of `rawValue` of Swift
> enum;
> - Use per case initializer like [Java
> Enum](https://docs.oracle.com/javase/tutorial/java/javaOO/enum.html).
> Swift enum uses custom initializer syntax to setup instances, not
> cases. So this approach is not suitable for Swift.
>
>
> On Sun, May 29, 2016 at 3:42 PM, Leonardo Pessoa <me at lmpessoa.com> wrote:
>> I think that's the case with enums. You're changing their current
>> behaviour of only having stored values to one in which it's computed (even
>> if only once and then stored). Enums are IMO something that have a static
>> value you know beforehand and can count on. That's why I'm not fond of the
>> accessor proposal. Otherwise I think we're transforming enums into a closed
>> set of struct instances and one could do that already by using a private
>> init.
>>
>>
>>> On 29 May 2016, at 3:38 am, Jānis Kiršteins via swift-evolution
>>> <swift-evolution at swift.org> wrote:
>>>
>>> I agree with the argument about use of "where", not replacing the raw
>>> value and having some kind of initialization block. But I cannot see
>>> why "accessors" concept is any better than stored properties to solve
>>> the particular problem. The "accessors" concept has much wider scope
>>> than enums and is a separate proposal.
>>>
>>> On Sat, May 28, 2016 at 11:39 PM, Brent Royal-Gordon
>>> <brent at architechies.com> wrote:
>>>>>> - Abusing rawValue is just that: an abuse.
>>>>>
>>>>> My original proposal does not replace rawValue and is compatible with
>>>>> it.
>>>>
>>>> `rawValue` has a different purpose from how you're using it. It's
>>>> supposed to allow you to convert your type to some other *equivalent* type,
>>>> like an equivalent integer or string. Moreover, it's supposed to allow you
>>>> to *reconstruct* the instance from the raw value—remember,
>>>> `RawRepresentable` has an `init(rawValue:)` requirement.
>>>>
>>>> It is *not* supposed to be an ancillary bag of information on the side.
>>>> You're cramming a square peg into a round hole here.
>>>>
>>>> (Also, if you use `rawValue` for an ancillary bag of information, that
>>>> means you *can't* use it on the same type for its intended purpose. For
>>>> instance, you would not be able to assign numbers to your Planet enum's
>>>> cases to help you serialize them or bridge them to Objective-C. That's not
>>>> good.)
>>>>
>>>>>> - Using `where` just doesn't match the use of `where` elsewhere in the
>>>>>> language; everywhere else, it's some kind of condition.
>>>>>
>>>>> It is also used in generic type constraints. Plus it reads like human
>>>>> language: `case mercury where (mass: 3.303e+23, radius: 2.4397e6)`
>>>>
>>>> But a generic constraint is also a type of condition: it specifies types
>>>> which are permitted and divides them from types that are not.
>>>>
>>>> This is *not* a condition. It's not anything like a condition. It's
>>>> simply not consistent with anything else in the language.
>>>>
>>>>>> - Dictionaries are the most straightforward way to handle this with
>>>>>> the current language, but their lack of exhaustiveness checking is a
>>>>>> problem.
>>>>>
>>>>> Dictionaries can be used as workaround, but they cannot (lack of
>>>>> exhaustiveness) solve the problem.
>>>>
>>>> I agree that they're a halfway solution.
>>>>
>>>> If `ValuesEnumerable` were to be accepted (and to have a generic
>>>> requirement for its `allValues` property), you could write a Dictionary-like
>>>> type which ensured at initialization time that it was exhaustive. That's not
>>>> as good as compile time, but it's not bad—sort of a three-quarters solution.
>>>>
>>>>        struct ExhaustiveDictionary<Key: Hashable, Value where Key:
>>>> ValuesEnumerable>: Collection, DictionaryLiteralConvertible {
>>>>                private var dictionary: [Key: Value]
>>>>
>>>>                init(dictionaryLiteral elements: (Key, Value)...) {
>>>>                        dictionary = [:]
>>>>                        for (k, v) in elements {
>>>>                                dictionary[k] = v
>>>>                        }
>>>>
>>>>                        if dictionary.count != Key.allValues.count {
>>>>                                let missingKeys = Key.allValues.filter {
>>>> dictionary[$0] == nil }
>>>>                                preconditionFailure("ExhaustiveDictionary
>>>> is missing elements from \(Key.self): \(missingKeys)")
>>>>                        }
>>>>                }
>>>>
>>>>                var startIndex: Dictionary.Index {
>>>>                        return dictionary.startIndex
>>>>                }
>>>>                var endIndex: Dictionary.Index {
>>>>                        return dictionary.endIndex
>>>>                }
>>>>                subscript(index: Dictionary.Index) -> (Key, Value) {
>>>>                        return dictionary[index]
>>>>                }
>>>>                func index(after i: Dictionary.Index) -> Dictionary.Index
>>>> {
>>>>                        return dictionary.index(after: i)
>>>>                }
>>>>
>>>>                subscript(key: Key) -> Value {
>>>>                        get { return dictionary[key]! }
>>>>                        set { dictionary[key] = newValue }
>>>>                }
>>>>        }
>>>>
>>>>>> What I would do is borrow the "accessors" concept from the property
>>>>>> behaviors proposal and extend it so that it supported both functions and
>>>>>> variables.
>>>>>
>>>>> Wouldn't accessor just be a redundant keyword here? Currently enums do
>>>>> not support stored properties, so I guess there is no extra need to
>>>>> mark properties with any special keyword.
>>>>
>>>> The keyword is mainly to indicate the unusual syntax at the definition
>>>> site, where you only have to specify the name of the accessor you're
>>>> defining, not a `func` or `var` keyword, a return type, or even parameter
>>>> names. (Like `willSet`, there's a default parameter name you can use.)
>>>> Secondarily, though, I think it's helpful to indicate very explicitly that
>>>> this is not an ordinary method or property definition, even if the compiler
>>>> could perhaps sort things out without it. `accessor` is something a user can
>>>> Google if they've never seen it before.
>>>>
>>>>> Property accessors might work for enums with associated values, but
>>>>> not so well without them.
>>>>
>>>> The two have nothing to do with each other. I showed your planets
>>>> example, which has no associated values but uses accessors just fine.
>>>>
>>>> --
>>>> Brent Royal-Gordon
>>>> Architechies
>>>>
>>> _______________________________________________
>>> swift-evolution mailing list
>>> swift-evolution at swift.org
>>> https://lists.swift.org/mailman/listinfo/swift-evolution


More information about the swift-evolution mailing list