Allowing enum extensions to also be able to expand case options


(Edward Valentini) #1

I am finding myself in a situation where the most elegant "swifty" solution would be to allow enum extensions to add to existing case options. For example lets say I'm using a library that has the following enum defined:

enum MyDirection {
   case east, west
}

My app for example also makes use of north and south, so I would love to be able to write:

extension MyDirection {
   case north,south
}

In objective c, one would probably have defined constants like MyDirectionEast etc... these would probably have been mapped to ints or strings so a consumer of this library could have easily extended this to add additional functionality, but using constants like that is not very "swifty"

I'm curious what the swift community thinks.

Thank you


(David Sweeris) #2

By itself, this would break switch statements, since they have to be exhaustive.

If anyone has any ideas about how to fix that, I'm all ears.

- Dave Sweeris

···

On Jun 30, 2016, at 14:58, Edward Valentini via swift-evolution <swift-evolution@swift.org> wrote:

I am finding myself in a situation where the most elegant "swifty" solution would be to allow enum extensions to add to existing case options. For example lets say I'm using a library that has the following enum defined:

enum MyDirection {
  case east, west
}

My app for example also makes use of north and south, so I would love to be able to write:

extension MyDirection {
  case north,south
}

In objective c, one would probably have defined constants like MyDirectionEast etc... these would probably have been mapped to ints or strings so a consumer of this library could have easily extended this to add additional functionality, but using constants like that is not very "swifty"

I'm curious what the swift community thinks.

Thank you
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Edward Valentini) #3

Existing switch statements would have to be modified to add a default statement. Additionally enums could be marked final to disallow case option expansion

···

On Jun 30, 2016, at 16:04, David Sweeris <davesweeris@mac.com> wrote:

By itself, this would break switch statements, since they have to be exhaustive.

If anyone has any ideas about how to fix that, I'm all ears.

- Dave Sweeris

On Jun 30, 2016, at 14:58, Edward Valentini via swift-evolution <swift-evolution@swift.org> wrote:

I am finding myself in a situation where the most elegant "swifty" solution would be to allow enum extensions to add to existing case options. For example lets say I'm using a library that has the following enum defined:

enum MyDirection {
case east, west
}

My app for example also makes use of north and south, so I would love to be able to write:

extension MyDirection {
case north,south
}

In objective c, one would probably have defined constants like MyDirectionEast etc... these would probably have been mapped to ints or strings so a consumer of this library could have easily extended this to add additional functionality, but using constants like that is not very "swifty"

I'm curious what the swift community thinks.

Thank you
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Dan Appel) #4

I've had a draft of a proposal lying around for a while which addresses
exactly this, but I haven't gotten around to sending it out for comments
yet. Link
<https://gist.github.com/Danappelxx/41b7c2e86787f75698bd48135cc616f5>.

Would appreciate if you guys took a look.
Dan Appel

Pasted inline below

Extensible Enums

   - Proposal: SE-NNNN
   <https://github.com/apple/swift-evolution/blob/master/proposals/NNNN-name.md>
   - Author: Dan Appel <https://github.com/danappelxx>
   - Status: Awaiting review
   <https://gist.github.com/Danappelxx/41b7c2e86787f75698bd48135cc616f5#rationale>
   - Review manager: TBD

<https://gist.github.com/Danappelxx/41b7c2e86787f75698bd48135cc616f5#introduction>
Introduction

This proposal introduces a new keyword that can be applied to enums which
allows new cases to be introduced in extensions.

Swift-evolution thread: [RFC] Extensible Enums
<https://lists.swift.org/pipermail/swift-evolution>
<https://gist.github.com/Danappelxx/41b7c2e86787f75698bd48135cc616f5#motivation>
Motivation

Enums are a powerful feature which provides a lot of benefit if you have a
limited number of behaviors. For example, associated values provide the
ability to make every case essentially a separate type. However, due to the
static nature of enums, they cannot be used in situations where they would
otherwise be a perfect fit.

An example of this would be the use of an Error enum like so:

enum FileError: ErrorProtocol {
    case fileNotFound(path: String)
    case corruptedFile(bytes: [Int8])
}func readFile() throws { ... }
// elsewhere in the codebasedo {
    try readFile()
} catch let error as FileError {
    switch error {
        case .fileNotFound(let path): // handle error
        case .corruptedFile(let bytes): // handle error
    }
} catch { ... }

While this is generally a good approach, it can be very dangerous for
library consumers if the author exposes the error to the user. This is due
to the fact that the switch statement has to be exhaustive and is only
satisfied when all enum cases have been accounted for. What this means for
library authors is that every time they add a new case to a public enum,
they are breaking the exhaustivity of the switch and making their library
backwards-incompatible.

Currently, the best workaround is to use a struct with static instances and
overloading the ~= operator. This allows for similar switch behavior but
overall is much less flexible, missing key features such as associated
values.

Another example is when the library is split into multiple modules, where
the error is defined in the first module and the second module wants to add
some error cases. An enum is very rarely used in this case because you
cannot add cases in other modules. Instead, library authors either use an
error protocol, and add more types that conform to it, or use the struct
approach shown above. While this is not terrible, adding cases in
extensions would better translate the intention of the author and adds more
flexiblity.
<https://gist.github.com/Danappelxx/41b7c2e86787f75698bd48135cc616f5#proposed-solution>Proposed
solution

The solution proposed is quite simple: add an extensible keyword/modifier
that can be applied to enums, which would require the default case when
switched on and allow new cases to be added in extensions.

Here is the translation of the very first example to the use an extensible enum
instead, with a new case added:

extensible enum ThingError: ErrorProtocol {
    case fileNotFound(path: String)
    case corruptedFile(bytes: [Int8])
    case failedReadingFile
}func readFile() throws { ... }
// elsewhere in the codebasedo {
    try readFile()
} catch let error as ThingError {
    switch error {
        case .fileNotFound(let path): // handle error
        case .corruptedFile(let bytes): // handle error
        default: // handle future errors that don't exist yet
    }
} catch { ... }

For the second example, we can simply extend the enum in the higher-level
module.

// Module FileProtocol

extensible enum FileError: ErrorProtocol {
    case fileNotFound(path: String)
}
protocol FileProtocol {
    func read() throws
}
// Module File
extension FileError {
    case corruptedFile(bytes: [Int8])
    case failedReadingFile
}
struct File: FileProtocol {
    func read() throws { ... }
}

<https://gist.github.com/Danappelxx/41b7c2e86787f75698bd48135cc616f5#detailed-design>Detailed
design

A new keyword would be added to the language which is only allowed in front
of the enum keyword. When an enum is marked extensible, new cases can be
added in extensions and switches that are performed on it require a default
case.
<https://gist.github.com/Danappelxx/41b7c2e86787f75698bd48135cc616f5#impact-on-existing-code>Impact
on existing code

There is no impact on existing code since this is purely an additive
feature.
<https://gist.github.com/Danappelxx/41b7c2e86787f75698bd48135cc616f5#alternatives-considered>Alternatives
considered

No alternatives have been considered (yet).

···

On Thu, Jun 30, 2016 at 1:04 PM David Sweeris via swift-evolution < swift-evolution@swift.org> wrote:

By itself, this would break switch statements, since they have to be
exhaustive.

If anyone has any ideas about how to fix that, I'm all ears.

- Dave Sweeris

> On Jun 30, 2016, at 14:58, Edward Valentini via swift-evolution < > swift-evolution@swift.org> wrote:
>
>
> I am finding myself in a situation where the most elegant "swifty"
solution would be to allow enum extensions to add to existing case
options. For example lets say I'm using a library that has the following
enum defined:
>
> enum MyDirection {
> case east, west
> }
>
> My app for example also makes use of north and south, so I would love to
be able to write:
>
> extension MyDirection {
> case north,south
> }
>
> In objective c, one would probably have defined constants like
MyDirectionEast etc... these would probably have been mapped to ints or
strings so a consumer of this library could have easily extended this to
add additional functionality, but using constants like that is not very
"swifty"
>
> I'm curious what the swift community thinks.
>
> Thank you
> _______________________________________________
> swift-evolution mailing list
> swift-evolution@swift.org
> https://lists.swift.org/mailman/listinfo/swift-evolution
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

--
Dan Appel


(Chris Lattner) #5

This is an additive proposal, so the core team would prefer you to wait until after Swift 3.0 to discuss this. Thanks,

-Chris

···

On Jun 30, 2016, at 12:58 PM, Edward Valentini via swift-evolution <swift-evolution@swift.org> wrote:

I am finding myself in a situation where the most elegant "swifty" solution would be to allow enum extensions to add to existing case options. For example lets say I'm using a library that has the following enum defined:


(Daniel Duan) #6

Edward Valentini via swift-evolution <swift-evolution@...> writes:

-1. Having a proper sum type so that the compiler can verify all cases are
covered is important. If you need a type of extensible tagged value,
I recommend using static members:

struct MyDirection {
    static let east = MyDirection()
    static let west = MyDirection()
}

extension MyDirection {
    static let north = MyDirection()
    static let south = MyDirection()
}

Sum type: https://en.wikipedia.org/wiki/Tagged_union


(Edward Valentini) #7

I really like the idea of making it opt in with the extensible keyword as opposed to opt out with final so this way there is no impact on existing code

···

On Jun 30, 2016, at 16:15, Dan Appel <dan.appel00@gmail.com> wrote:

I've had a draft of a proposal lying around for a while which addresses exactly this, but I haven't gotten around to sending it out for comments yet. Link.

Would appreciate if you guys took a look.
Dan Appel

Pasted inline below

Extensible Enums
Proposal: SE-NNNN
Author: Dan Appel
Status: Awaiting review
Review manager: TBD
Introduction

This proposal introduces a new keyword that can be applied to enums which allows new cases to be introduced in extensions.

Swift-evolution thread: [RFC] Extensible Enums

Motivation

Enums are a powerful feature which provides a lot of benefit if you have a limited number of behaviors. For example, associated values provide the ability to make every case essentially a separate type. However, due to the static nature of enums, they cannot be used in situations where they would otherwise be a perfect fit.

An example of this would be the use of an Error enum like so:

enum FileError: ErrorProtocol {
    case fileNotFound(path: String)
    case corruptedFile(bytes: [Int8])
}
func readFile() throws { ... }

// elsewhere in the codebase
do {
    try readFile()
} catch let error as FileError {
    switch error {
        case .fileNotFound(let path): // handle error
        case .corruptedFile(let bytes): // handle error
    }
} catch { ... }
While this is generally a good approach, it can be very dangerous for library consumers if the author exposes the error to the user. This is due to the fact that the switch statement has to be exhaustive and is only satisfied when all enum cases have been accounted for. What this means for library authors is that every time they add a new case to a public enum, they are breaking the exhaustivity of the switch and making their library backwards-incompatible.

Currently, the best workaround is to use a struct with static instances and overloading the ~= operator. This allows for similar switch behavior but overall is much less flexible, missing key features such as associated values.

Another example is when the library is split into multiple modules, where the error is defined in the first module and the second module wants to add some error cases. An enum is very rarely used in this case because you cannot add cases in other modules. Instead, library authors either use an error protocol, and add more types that conform to it, or use the struct approach shown above. While this is not terrible, adding cases in extensions would better translate the intention of the author and adds more flexiblity.

Proposed solution

The solution proposed is quite simple: add an extensible keyword/modifier that can be applied to enums, which would require the default case when switched on and allow new cases to be added in extensions.

Here is the translation of the very first example to the use an extensible enum instead, with a new case added:

extensible enum ThingError: ErrorProtocol {
    case fileNotFound(path: String)
    case corruptedFile(bytes: [Int8])
    case failedReadingFile
}
func readFile() throws { ... }

// elsewhere in the codebase
do {
    try readFile()
} catch let error as ThingError {
    switch error {
        case .fileNotFound(let path): // handle error
        case .corruptedFile(let bytes): // handle error
        default: // handle future errors that don't exist yet
    }
} catch { ... }
For the second example, we can simply extend the enum in the higher-level module.

// Module FileProtocol

extensible enum FileError: ErrorProtocol {
    case fileNotFound(path: String)
}

protocol FileProtocol {
    func read() throws
}

// Module File

extension FileError {
    case corruptedFile(bytes: [Int8])
    case failedReadingFile
}

struct File: FileProtocol {
    func read() throws { ... }
}
Detailed design

A new keyword would be added to the language which is only allowed in front of the enum keyword. When an enum is marked extensible, new cases can be added in extensions and switches that are performed on it require a defaultcase.

Impact on existing code

There is no impact on existing code since this is purely an additive feature.

Alternatives considered

No alternatives have been considered (yet).

On Thu, Jun 30, 2016 at 1:04 PM David Sweeris via swift-evolution <swift-evolution@swift.org> wrote:
By itself, this would break switch statements, since they have to be exhaustive.

If anyone has any ideas about how to fix that, I'm all ears.

- Dave Sweeris

> On Jun 30, 2016, at 14:58, Edward Valentini via swift-evolution <swift-evolution@swift.org> wrote:
>
>
> I am finding myself in a situation where the most elegant "swifty" solution would be to allow enum extensions to add to existing case options. For example lets say I'm using a library that has the following enum defined:
>
> enum MyDirection {
> case east, west
> }
>
> My app for example also makes use of north and south, so I would love to be able to write:
>
> extension MyDirection {
> case north,south
> }
>
> In objective c, one would probably have defined constants like MyDirectionEast etc... these would probably have been mapped to ints or strings so a consumer of this library could have easily extended this to add additional functionality, but using constants like that is not very "swifty"
>
> I'm curious what the swift community thinks.
>
> Thank you
> _______________________________________________
> swift-evolution mailing list
> swift-evolution@swift.org
> https://lists.swift.org/mailman/listinfo/swift-evolution
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

--
Dan Appel


(Guillermo Peralta Scura) #8

I think we have the same question as with the "sealed" classes by default
proposal. To allow or not extension of the public API by the user (at least
by default).

···

El jue., 30 jun. 2016 a las 16:10, Edward Valentini via swift-evolution (< swift-evolution@swift.org>) escribió:

Existing switch statements would have to be modified to add a default
statement. Additionally enums could be marked final to disallow case
option expansion

On Jun 30, 2016, at 16:04, David Sweeris <davesweeris@mac.com> wrote:
>
> By itself, this would break switch statements, since they have to be
exhaustive.
>
> If anyone has any ideas about how to fix that, I'm all ears.
>
> - Dave Sweeris
>
>> On Jun 30, 2016, at 14:58, Edward Valentini via swift-evolution < > swift-evolution@swift.org> wrote:
>>
>>
>> I am finding myself in a situation where the most elegant "swifty"
solution would be to allow enum extensions to add to existing case
options. For example lets say I'm using a library that has the following
enum defined:
>>
>> enum MyDirection {
>> case east, west
>> }
>>
>> My app for example also makes use of north and south, so I would love
to be able to write:
>>
>> extension MyDirection {
>> case north,south
>> }
>>
>> In objective c, one would probably have defined constants like
MyDirectionEast etc... these would probably have been mapped to ints or
strings so a consumer of this library could have easily extended this to
add additional functionality, but using constants like that is not very
"swifty"
>>
>> I'm curious what the swift community thinks.
>>
>> Thank you
>> _______________________________________________
>> swift-evolution mailing list
>> swift-evolution@swift.org
>> https://lists.swift.org/mailman/listinfo/swift-evolution
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Guillermo Peralta Scura) #9

I think the approach taken by your proporsal is really good. Would love to
have that feature for the language.

···

El jue., 30 jun. 2016 a las 16:19, Edward Valentini via swift-evolution (< swift-evolution@swift.org>) escribió:

I really like the idea of making it opt in with the extensible keyword as
opposed to opt out with final so this way there is no impact on existing
code

On Jun 30, 2016, at 16:15, Dan Appel <dan.appel00@gmail.com> wrote:

I've had a draft of a proposal lying around for a while which addresses
exactly this, but I haven't gotten around to sending it out for comments
yet. Link
<https://gist.github.com/Danappelxx/41b7c2e86787f75698bd48135cc616f5>.

Would appreciate if you guys took a look.
Dan Appel

Pasted inline below

Extensible Enums

   - Proposal: SE-NNNN
   <https://github.com/apple/swift-evolution/blob/master/proposals/NNNN-name.md>
   - Author: Dan Appel <https://github.com/danappelxx>
   - Status: Awaiting review
   <https://gist.github.com/Danappelxx/41b7c2e86787f75698bd48135cc616f5#rationale>
   - Review manager: TBD

<https://gist.github.com/Danappelxx/41b7c2e86787f75698bd48135cc616f5#introduction>
Introduction

This proposal introduces a new keyword that can be applied to enums which
allows new cases to be introduced in extensions.

Swift-evolution thread: [RFC] Extensible Enums
<https://lists.swift.org/pipermail/swift-evolution>

<https://gist.github.com/Danappelxx/41b7c2e86787f75698bd48135cc616f5#motivation>
Motivation

Enums are a powerful feature which provides a lot of benefit if you have a
limited number of behaviors. For example, associated values provide the
ability to make every case essentially a separate type. However, due to the
static nature of enums, they cannot be used in situations where they would
otherwise be a perfect fit.

An example of this would be the use of an Error enum like so:

enum FileError: ErrorProtocol {
    case fileNotFound(path: String)
    case corruptedFile(bytes: [Int8])
}func readFile() throws { ... }
// elsewhere in the codebasedo {
    try readFile()
} catch let error as FileError {
    switch error {
        case .fileNotFound(let path): // handle error
        case .corruptedFile(let bytes): // handle error
    }
} catch { ... }

While this is generally a good approach, it can be very dangerous for
library consumers if the author exposes the error to the user. This is due
to the fact that the switch statement has to be exhaustive and is only
satisfied when all enum cases have been accounted for. What this means for
library authors is that every time they add a new case to a public enum,
they are breaking the exhaustivity of the switch and making their library
backwards-incompatible.

Currently, the best workaround is to use a struct with static instances
and overloading the ~= operator. This allows for similar switch behavior
but overall is much less flexible, missing key features such as associated
values.

Another example is when the library is split into multiple modules, where
the error is defined in the first module and the second module wants to add
some error cases. An enum is very rarely used in this case because you
cannot add cases in other modules. Instead, library authors either use an
error protocol, and add more types that conform to it, or use the struct
approach shown above. While this is not terrible, adding cases in
extensions would better translate the intention of the author and adds more
flexiblity.

<https://gist.github.com/Danappelxx/41b7c2e86787f75698bd48135cc616f5#proposed-solution>Proposed
solution

The solution proposed is quite simple: add an extensible keyword/modifier
that can be applied to enums, which would require the default case when
switched on and allow new cases to be added in extensions.

Here is the translation of the very first example to the use an extensible enum
instead, with a new case added:

extensible enum ThingError: ErrorProtocol {
    case fileNotFound(path: String)
    case corruptedFile(bytes: [Int8])
    case failedReadingFile
}func readFile() throws { ... }
// elsewhere in the codebasedo {
    try readFile()
} catch let error as ThingError {
    switch error {
        case .fileNotFound(let path): // handle error
        case .corruptedFile(let bytes): // handle error
        default: // handle future errors that don't exist yet
    }
} catch { ... }

For the second example, we can simply extend the enum in the higher-level
module.

// Module FileProtocol

extensible enum FileError: ErrorProtocol {
    case fileNotFound(path: String)
}
protocol FileProtocol {
    func read() throws
}
// Module File
extension FileError {
    case corruptedFile(bytes: [Int8])
    case failedReadingFile
}
struct File: FileProtocol {
    func read() throws { ... }
}

<https://gist.github.com/Danappelxx/41b7c2e86787f75698bd48135cc616f5#detailed-design>Detailed
design

A new keyword would be added to the language which is only allowed in
front of the enum keyword. When an enum is marked extensible, new cases
can be added in extensions and switches that are performed on it require a
defaultcase.

<https://gist.github.com/Danappelxx/41b7c2e86787f75698bd48135cc616f5#impact-on-existing-code>Impact
on existing code

There is no impact on existing code since this is purely an additive
feature.

<https://gist.github.com/Danappelxx/41b7c2e86787f75698bd48135cc616f5#alternatives-considered>Alternatives
considered

No alternatives have been considered (yet).

On Thu, Jun 30, 2016 at 1:04 PM David Sweeris via swift-evolution < > swift-evolution@swift.org> wrote:

By itself, this would break switch statements, since they have to be
exhaustive.

If anyone has any ideas about how to fix that, I'm all ears.

- Dave Sweeris

> On Jun 30, 2016, at 14:58, Edward Valentini via swift-evolution < >> swift-evolution@swift.org> wrote:
>
>
> I am finding myself in a situation where the most elegant "swifty"
solution would be to allow enum extensions to add to existing case
options. For example lets say I'm using a library that has the following
enum defined:
>
> enum MyDirection {
> case east, west
> }
>
> My app for example also makes use of north and south, so I would love
to be able to write:
>
> extension MyDirection {
> case north,south
> }
>
> In objective c, one would probably have defined constants like
MyDirectionEast etc... these would probably have been mapped to ints or
strings so a consumer of this library could have easily extended this to
add additional functionality, but using constants like that is not very
"swifty"
>
> I'm curious what the swift community thinks.
>
> Thank you
> _______________________________________________
> swift-evolution mailing list
> swift-evolution@swift.org
> https://lists.swift.org/mailman/listinfo/swift-evolution
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

--
Dan Appel

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Dan Appel) #10

That makes sense, thanks for letting us know.

Dan

···

On Thu, Jun 30, 2016 at 11:32 PM Chris Lattner via swift-evolution < swift-evolution@swift.org> wrote:

> On Jun 30, 2016, at 12:58 PM, Edward Valentini via swift-evolution < > swift-evolution@swift.org> wrote:
>
>
> I am finding myself in a situation where the most elegant "swifty"
solution would be to allow enum extensions to add to existing case
options. For example lets say I'm using a library that has the following
enum defined:

This is an additive proposal, so the core team would prefer you to wait
until after Swift 3.0 to discuss this. Thanks,

-Chris

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

--
Dan Appel


(Paul Cantrell) #11

While it doesn’t give all the “raw value” functionality of enum, it’s possible to use object instance uniqueness to get enum-like behavior that can be extended:

    public protocol OpenEnum: class, Hashable { }

    extension OpenEnum {
      public var hashValue: Int {
        return ObjectIdentifier(self).hashValue
      }
    }

    public func ==<T: OpenEnum>(lhs: T, rhs: T) -> Bool {
      return lhs === rhs
    }

A library can provide:

    public final class Color: OpenEnum, CustomStringConvertible {
      public let description: String

      public init(description: String) {
        self.description = description
      }

      static let
        black = Color(description: "black"),
        white = Color(description: "white")
    }

And then in a client project:

    extension Color {
      static let
        puce = Color(description: "puce"),
        mauve = Color(description: "mauve"),
        fuchsia = Color(description: "fuchsia")
    }

(This is how Siesta provides an extensible set of pipeline stages. https://github.com/bustoutsolutions/siesta/pull/64)

With this approach, you still get the .member shortcut in some circumstances:

    let eyebleedPalette: [Color] = [.fuchsia, .black, .mauve]

…but not in others:

    // Compiles
    switch(color) {
      case Color.red: print("Danger!")
      case Color.mauve: print("Dancing!")
      default: print("Nothing notable")
    }

    // Does not compile
    switch(color) {
      case .red: print("Danger!")
      case .mauve: print("Dancing!")
      default: print("Nothing notable")
    }

Given that this already comes close to giving the sort of functionality one would want out of an extensible enum, perhaps it’s better to fill out the gaps in this approach instead of adding a new language feature? This would have the advantage of not adding a keyword, and presumably provide useful behaviors that generalize to patterns other than extensible enums.

Cheers,

Paul

···

On Jun 30, 2016, at 3:23 PM, Guillermo Peralta Scura via swift-evolution <swift-evolution@swift.org> wrote:

I think the approach taken by your proporsal is really good. Would love to have that feature for the language.

El jue., 30 jun. 2016 a las 16:19, Edward Valentini via swift-evolution (<swift-evolution@swift.org <mailto:swift-evolution@swift.org>>) escribió:

I really like the idea of making it opt in with the extensible keyword as opposed to opt out with final so this way there is no impact on existing code

On Jun 30, 2016, at 16:15, Dan Appel <dan.appel00@gmail.com <mailto:dan.appel00@gmail.com>> wrote:

I've had a draft of a proposal lying around for a while which addresses exactly this, but I haven't gotten around to sending it out for comments yet. Link <https://gist.github.com/Danappelxx/41b7c2e86787f75698bd48135cc616f5>.

Would appreciate if you guys took a look.
Dan Appel

Pasted inline below

Extensible Enums

Proposal: SE-NNNN <https://github.com/apple/swift-evolution/blob/master/proposals/NNNN-name.md>
Author: Dan Appel <https://github.com/danappelxx>
Status: Awaiting review <https://gist.github.com/Danappelxx/41b7c2e86787f75698bd48135cc616f5#rationale>
Review manager: TBD
<https://gist.github.com/Danappelxx/41b7c2e86787f75698bd48135cc616f5#introduction>Introduction

This proposal introduces a new keyword that can be applied to enums which allows new cases to be introduced in extensions.

Swift-evolution thread: [RFC] Extensible Enums <https://lists.swift.org/pipermail/swift-evolution>
<https://gist.github.com/Danappelxx/41b7c2e86787f75698bd48135cc616f5#motivation>Motivation

Enums are a powerful feature which provides a lot of benefit if you have a limited number of behaviors. For example, associated values provide the ability to make every case essentially a separate type. However, due to the static nature of enums, they cannot be used in situations where they would otherwise be a perfect fit.

An example of this would be the use of an Error enum like so:

enum FileError: ErrorProtocol {
    case fileNotFound(path: String)
    case corruptedFile(bytes: [Int8])
}
func readFile() throws { ... }

// elsewhere in the codebase
do {
    try readFile()
} catch let error as FileError {
    switch error {
        case .fileNotFound(let path): // handle error
        case .corruptedFile(let bytes): // handle error
    }
} catch { ... }
While this is generally a good approach, it can be very dangerous for library consumers if the author exposes the error to the user. This is due to the fact that the switch statement has to be exhaustive and is only satisfied when all enum cases have been accounted for. What this means for library authors is that every time they add a new case to a public enum, they are breaking the exhaustivity of the switch and making their library backwards-incompatible.

Currently, the best workaround is to use a struct with static instances and overloading the ~= operator. This allows for similar switch behavior but overall is much less flexible, missing key features such as associated values.

Another example is when the library is split into multiple modules, where the error is defined in the first module and the second module wants to add some error cases. An enum is very rarely used in this case because you cannot add cases in other modules. Instead, library authors either use an error protocol, and add more types that conform to it, or use the struct approach shown above. While this is not terrible, adding cases in extensions would better translate the intention of the author and adds more flexiblity.

<https://gist.github.com/Danappelxx/41b7c2e86787f75698bd48135cc616f5#proposed-solution>Proposed solution

The solution proposed is quite simple: add an extensible keyword/modifier that can be applied to enums, which would require the default case when switched on and allow new cases to be added in extensions.

Here is the translation of the very first example to the use an extensible enum instead, with a new case added:

extensible enum ThingError: ErrorProtocol {
    case fileNotFound(path: String)
    case corruptedFile(bytes: [Int8])
    case failedReadingFile
}
func readFile() throws { ... }

// elsewhere in the codebase
do {
    try readFile()
} catch let error as ThingError {
    switch error {
        case .fileNotFound(let path): // handle error
        case .corruptedFile(let bytes): // handle error
        default: // handle future errors that don't exist yet
    }
} catch { ... }
For the second example, we can simply extend the enum in the higher-level module.

// Module FileProtocol

extensible enum FileError: ErrorProtocol {
    case fileNotFound(path: String)
}

protocol FileProtocol {
    func read() throws
}

// Module File

extension FileError {
    case corruptedFile(bytes: [Int8])
    case failedReadingFile
}

struct File: FileProtocol {
    func read() throws { ... }
}
<https://gist.github.com/Danappelxx/41b7c2e86787f75698bd48135cc616f5#detailed-design>Detailed design

A new keyword would be added to the language which is only allowed in front of the enum keyword. When an enum is marked extensible, new cases can be added in extensions and switches that are performed on it require a defaultcase.

<https://gist.github.com/Danappelxx/41b7c2e86787f75698bd48135cc616f5#impact-on-existing-code>Impact on existing code

There is no impact on existing code since this is purely an additive feature.

<https://gist.github.com/Danappelxx/41b7c2e86787f75698bd48135cc616f5#alternatives-considered>Alternatives considered

No alternatives have been considered (yet).

On Thu, Jun 30, 2016 at 1:04 PM David Sweeris via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:
By itself, this would break switch statements, since they have to be exhaustive.

If anyone has any ideas about how to fix that, I'm all ears.

- Dave Sweeris

> On Jun 30, 2016, at 14:58, Edward Valentini via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:
>
>
> I am finding myself in a situation where the most elegant "swifty" solution would be to allow enum extensions to add to existing case options. For example lets say I'm using a library that has the following enum defined:
>
> enum MyDirection {
> case east, west
> }
>
> My app for example also makes use of north and south, so I would love to be able to write:
>
> extension MyDirection {
> case north,south
> }
>
> In objective c, one would probably have defined constants like MyDirectionEast etc... these would probably have been mapped to ints or strings so a consumer of this library could have easily extended this to add additional functionality, but using constants like that is not very "swifty"
>
> I'm curious what the swift community thinks.
>
> Thank you
> _______________________________________________
> swift-evolution mailing list
> swift-evolution@swift.org <mailto:swift-evolution@swift.org>
> https://lists.swift.org/mailman/listinfo/swift-evolution
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution
--
Dan Appel

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Dan Appel) #12

Paul,

That is the current workaround (as the proposal mentions), but it is still
missing support for enum features such as associated values and the pattern
matching power that they bring.

Also, by locking your OpenEnum conformers to reference types, you lose out
on the value-semantics (very important, even for enums), and bring in the
extra weight that a class is.

Dan

···

On Thu, Jun 30, 2016 at 1:42 PM Paul Cantrell <cantrell@pobox.com> wrote:

While it doesn’t give all the “raw value” functionality of enum, it’s
possible to use object instance uniqueness to get enum-like behavior that
can be extended:

    public protocol OpenEnum: class, Hashable { }

    extension OpenEnum {
      public var hashValue: Int {
        return ObjectIdentifier(self).hashValue
      }
    }

    public func ==<T: OpenEnum>(lhs: T, rhs: T) -> Bool {
      return lhs === rhs
    }

A library can provide:

    public final class Color: OpenEnum, CustomStringConvertible {
      public let description: String

      public init(description: String) {
        self.description = description
      }

      static let
        black = Color(description: "black"),
        white = Color(description: "white")
    }

And then in a client project:

    extension Color {
      static let
        puce = Color(description: "puce"),
        mauve = Color(description: "mauve"),
        fuchsia = Color(description: "fuchsia")
    }

(This is how Siesta provides an extensible set of pipeline stages.
https://github.com/bustoutsolutions/siesta/pull/64)

With this approach, you still get the .member shortcut in some
circumstances:

    let eyebleedPalette: [Color] = [.fuchsia, .black, .mauve]

…but not in others:

    // Compiles
    switch(color) {
      case *Color*.red: print("Danger!")
      case *Color*.mauve: print("Dancing!")
      default: print("Nothing notable")
    }

    // Does not compile
    switch(color) {
      case .red: print("Danger!")
      case .mauve: print("Dancing!")
      default: print("Nothing notable")
    }

Given that this already comes close to giving the sort of functionality
one would want out of an extensible enum, perhaps it’s better to fill out
the gaps in this approach instead of adding a new language feature? This
would have the advantage of not adding a keyword, and presumably provide
useful behaviors that generalize to patterns other than extensible enums.

Cheers,

Paul

On Jun 30, 2016, at 3:23 PM, Guillermo Peralta Scura via swift-evolution < > swift-evolution@swift.org> wrote:

I think the approach taken by your proporsal is really good. Would love to
have that feature for the language.

El jue., 30 jun. 2016 a las 16:19, Edward Valentini via swift-evolution (< > swift-evolution@swift.org>) escribió:

I really like the idea of making it opt in with the extensible keyword as
opposed to opt out with final so this way there is no impact on existing
code

On Jun 30, 2016, at 16:15, Dan Appel <dan.appel00@gmail.com> wrote:

I've had a draft of a proposal lying around for a while which addresses
exactly this, but I haven't gotten around to sending it out for comments
yet. Link
<https://gist.github.com/Danappelxx/41b7c2e86787f75698bd48135cc616f5>.

Would appreciate if you guys took a look.
Dan Appel

Pasted inline below

Extensible Enums

   - Proposal: SE-NNNN
   <https://github.com/apple/swift-evolution/blob/master/proposals/NNNN-name.md>
   - Author: Dan Appel <https://github.com/danappelxx>
   - Status: Awaiting review
   <https://gist.github.com/Danappelxx/41b7c2e86787f75698bd48135cc616f5#rationale>
   - Review manager: TBD

<https://gist.github.com/Danappelxx/41b7c2e86787f75698bd48135cc616f5#introduction>
Introduction

This proposal introduces a new keyword that can be applied to enums which
allows new cases to be introduced in extensions.

Swift-evolution thread: [RFC] Extensible Enums
<https://lists.swift.org/pipermail/swift-evolution>

<https://gist.github.com/Danappelxx/41b7c2e86787f75698bd48135cc616f5#motivation>
Motivation

Enums are a powerful feature which provides a lot of benefit if you have
a limited number of behaviors. For example, associated values provide the
ability to make every case essentially a separate type. However, due to the
static nature of enums, they cannot be used in situations where they would
otherwise be a perfect fit.

An example of this would be the use of an Error enum like so:

enum FileError: ErrorProtocol {
    case fileNotFound(path: String)
    case corruptedFile(bytes: [Int8])
}func readFile() throws { ... }
// elsewhere in the codebasedo {
    try readFile()
} catch let error as FileError {
    switch error {
        case .fileNotFound(let path): // handle error
        case .corruptedFile(let bytes): // handle error
    }
} catch { ... }

While this is generally a good approach, it can be very dangerous for
library consumers if the author exposes the error to the user. This is due
to the fact that the switch statement has to be exhaustive and is only
satisfied when all enum cases have been accounted for. What this means for
library authors is that every time they add a new case to a public enum,
they are breaking the exhaustivity of the switch and making their
library backwards-incompatible.

Currently, the best workaround is to use a struct with static instances
and overloading the ~= operator. This allows for similar switch behavior
but overall is much less flexible, missing key features such as associated
values.

Another example is when the library is split into multiple modules, where
the error is defined in the first module and the second module wants to add
some error cases. An enum is very rarely used in this case because you
cannot add cases in other modules. Instead, library authors either use an
error protocol, and add more types that conform to it, or use the struct
approach shown above. While this is not terrible, adding cases in
extensions would better translate the intention of the author and adds more
flexiblity.

<https://gist.github.com/Danappelxx/41b7c2e86787f75698bd48135cc616f5#proposed-solution>Proposed
solution

The solution proposed is quite simple: add an extensible keyword/modifier
that can be applied to enums, which would require the default case when
switched on and allow new cases to be added in extensions.

Here is the translation of the very first example to the use an
extensible enum instead, with a new case added:

extensible enum ThingError: ErrorProtocol {
    case fileNotFound(path: String)
    case corruptedFile(bytes: [Int8])
    case failedReadingFile
}func readFile() throws { ... }
// elsewhere in the codebasedo {
    try readFile()
} catch let error as ThingError {
    switch error {
        case .fileNotFound(let path): // handle error
        case .corruptedFile(let bytes): // handle error
        default: // handle future errors that don't exist yet
    }
} catch { ... }

For the second example, we can simply extend the enum in the higher-level
module.

// Module FileProtocol

extensible enum FileError: ErrorProtocol {
    case fileNotFound(path: String)
}
protocol FileProtocol {
    func read() throws
}
// Module File
extension FileError {
    case corruptedFile(bytes: [Int8])
    case failedReadingFile
}
struct File: FileProtocol {
    func read() throws { ... }
}

<https://gist.github.com/Danappelxx/41b7c2e86787f75698bd48135cc616f5#detailed-design>Detailed
design

A new keyword would be added to the language which is only allowed in
front of the enum keyword. When an enum is marked extensible, new cases
can be added in extensions and switches that are performed on it require a
defaultcase.

<https://gist.github.com/Danappelxx/41b7c2e86787f75698bd48135cc616f5#impact-on-existing-code>Impact
on existing code

There is no impact on existing code since this is purely an additive
feature.

<https://gist.github.com/Danappelxx/41b7c2e86787f75698bd48135cc616f5#alternatives-considered>Alternatives
considered

No alternatives have been considered (yet).

On Thu, Jun 30, 2016 at 1:04 PM David Sweeris via swift-evolution < >> swift-evolution@swift.org> wrote:

By itself, this would break switch statements, since they have to be
exhaustive.

If anyone has any ideas about how to fix that, I'm all ears.

- Dave Sweeris

> On Jun 30, 2016, at 14:58, Edward Valentini via swift-evolution < >>> swift-evolution@swift.org> wrote:
>
>
> I am finding myself in a situation where the most elegant "swifty"
solution would be to allow enum extensions to add to existing case
options. For example lets say I'm using a library that has the following
enum defined:
>
> enum MyDirection {
> case east, west
> }
>
> My app for example also makes use of north and south, so I would love
to be able to write:
>
> extension MyDirection {
> case north,south
> }
>
> In objective c, one would probably have defined constants like
MyDirectionEast etc... these would probably have been mapped to ints or
strings so a consumer of this library could have easily extended this to
add additional functionality, but using constants like that is not very
"swifty"
>
> I'm curious what the swift community thinks.
>
> Thank you
> _______________________________________________
> swift-evolution mailing list
> swift-evolution@swift.org
> https://lists.swift.org/mailman/listinfo/swift-evolution
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

--
Dan Appel

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

--

Dan Appel


(David Waite) #13

Paul,

That is the current workaround (as the proposal mentions), but it is still missing support for enum features such as associated values and the pattern matching power that they bring.

I don’t believe a developer would be able to extend an enum to support arbitrary associated values, the same as the limitation that one cannot extend a type today to have extra members. Value types need to have an understood size and structure at compile time of the file/module that they are in.

···

On Jun 30, 2016, at 2:54 PM, Dan Appel via swift-evolution <swift-evolution@swift.org> wrote:

Dan

On Thu, Jun 30, 2016 at 1:42 PM Paul Cantrell <cantrell@pobox.com <mailto:cantrell@pobox.com>> wrote:
While it doesn’t give all the “raw value” functionality of enum, it’s possible to use object instance uniqueness to get enum-like behavior that can be extended:

    public protocol OpenEnum: class, Hashable { }

    extension OpenEnum {
      public var hashValue: Int {
        return ObjectIdentifier(self).hashValue
      }
    }

    public func ==<T: OpenEnum>(lhs: T, rhs: T) -> Bool {
      return lhs === rhs
    }

A library can provide:

    public final class Color: OpenEnum, CustomStringConvertible {
      public let description: String

      public init(description: String) {
        self.description = description
      }

      static let
        black = Color(description: "black"),
        white = Color(description: "white")
    }

And then in a client project:

    extension Color {
      static let
        puce = Color(description: "puce"),
        mauve = Color(description: "mauve"),
        fuchsia = Color(description: "fuchsia")
    }

(This is how Siesta provides an extensible set of pipeline stages. https://github.com/bustoutsolutions/siesta/pull/64)

With this approach, you still get the .member shortcut in some circumstances:

    let eyebleedPalette: [Color] = [.fuchsia, .black, .mauve]

…but not in others:

    // Compiles
    switch(color) {
      case Color.red: print("Danger!")
      case Color.mauve: print("Dancing!")
      default: print("Nothing notable")
    }

    // Does not compile
    switch(color) {
      case .red: print("Danger!")
      case .mauve: print("Dancing!")
      default: print("Nothing notable")
    }

Given that this already comes close to giving the sort of functionality one would want out of an extensible enum, perhaps it’s better to fill out the gaps in this approach instead of adding a new language feature? This would have the advantage of not adding a keyword, and presumably provide useful behaviors that generalize to patterns other than extensible enums.

Cheers,

Paul

On Jun 30, 2016, at 3:23 PM, Guillermo Peralta Scura via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

I think the approach taken by your proporsal is really good. Would love to have that feature for the language.

El jue., 30 jun. 2016 a las 16:19, Edward Valentini via swift-evolution (<swift-evolution@swift.org <mailto:swift-evolution@swift.org>>) escribió:

I really like the idea of making it opt in with the extensible keyword as opposed to opt out with final so this way there is no impact on existing code

On Jun 30, 2016, at 16:15, Dan Appel <dan.appel00@gmail.com <mailto:dan.appel00@gmail.com>> wrote:

I've had a draft of a proposal lying around for a while which addresses exactly this, but I haven't gotten around to sending it out for comments yet. Link <https://gist.github.com/Danappelxx/41b7c2e86787f75698bd48135cc616f5>.

Would appreciate if you guys took a look.
Dan Appel

Pasted inline below

Extensible Enums

Proposal: SE-NNNN <https://github.com/apple/swift-evolution/blob/master/proposals/NNNN-name.md>
Author: Dan Appel <https://github.com/danappelxx>
Status: Awaiting review <https://gist.github.com/Danappelxx/41b7c2e86787f75698bd48135cc616f5#rationale>
Review manager: TBD
<https://gist.github.com/Danappelxx/41b7c2e86787f75698bd48135cc616f5#introduction>Introduction

This proposal introduces a new keyword that can be applied to enums which allows new cases to be introduced in extensions.

Swift-evolution thread: [RFC] Extensible Enums <https://lists.swift.org/pipermail/swift-evolution>
<https://gist.github.com/Danappelxx/41b7c2e86787f75698bd48135cc616f5#motivation>Motivation

Enums are a powerful feature which provides a lot of benefit if you have a limited number of behaviors. For example, associated values provide the ability to make every case essentially a separate type. However, due to the static nature of enums, they cannot be used in situations where they would otherwise be a perfect fit.

An example of this would be the use of an Error enum like so:

enum FileError: ErrorProtocol {
    case fileNotFound(path: String)
    case corruptedFile(bytes: [Int8])
}
func readFile() throws { ... }

// elsewhere in the codebase
do {
    try readFile()
} catch let error as FileError {
    switch error {
        case .fileNotFound(let path): // handle error
        case .corruptedFile(let bytes): // handle error
    }
} catch { ... }
While this is generally a good approach, it can be very dangerous for library consumers if the author exposes the error to the user. This is due to the fact that the switch statement has to be exhaustive and is only satisfied when all enum cases have been accounted for. What this means for library authors is that every time they add a new case to a public enum, they are breaking the exhaustivity of the switch and making their library backwards-incompatible.

Currently, the best workaround is to use a struct with static instances and overloading the ~= operator. This allows for similar switch behavior but overall is much less flexible, missing key features such as associated values.

Another example is when the library is split into multiple modules, where the error is defined in the first module and the second module wants to add some error cases. An enum is very rarely used in this case because you cannot add cases in other modules. Instead, library authors either use an error protocol, and add more types that conform to it, or use the struct approach shown above. While this is not terrible, adding cases in extensions would better translate the intention of the author and adds more flexiblity.

<https://gist.github.com/Danappelxx/41b7c2e86787f75698bd48135cc616f5#proposed-solution>Proposed solution

The solution proposed is quite simple: add an extensible keyword/modifier that can be applied to enums, which would require the default case when switched on and allow new cases to be added in extensions.

Here is the translation of the very first example to the use an extensible enum instead, with a new case added:

extensible enum ThingError: ErrorProtocol {
    case fileNotFound(path: String)
    case corruptedFile(bytes: [Int8])
    case failedReadingFile
}
func readFile() throws { ... }

// elsewhere in the codebase
do {
    try readFile()
} catch let error as ThingError {
    switch error {
        case .fileNotFound(let path): // handle error
        case .corruptedFile(let bytes): // handle error
        default: // handle future errors that don't exist yet
    }
} catch { ... }
For the second example, we can simply extend the enum in the higher-level module.

// Module FileProtocol

extensible enum FileError: ErrorProtocol {
    case fileNotFound(path: String)
}

protocol FileProtocol {
    func read() throws
}

// Module File

extension FileError {
    case corruptedFile(bytes: [Int8])
    case failedReadingFile
}

struct File: FileProtocol {
    func read() throws { ... }
}
<https://gist.github.com/Danappelxx/41b7c2e86787f75698bd48135cc616f5#detailed-design>Detailed design

A new keyword would be added to the language which is only allowed in front of the enum keyword. When an enum is marked extensible, new cases can be added in extensions and switches that are performed on it require a defaultcase.

<https://gist.github.com/Danappelxx/41b7c2e86787f75698bd48135cc616f5#impact-on-existing-code>Impact on existing code

There is no impact on existing code since this is purely an additive feature.

<https://gist.github.com/Danappelxx/41b7c2e86787f75698bd48135cc616f5#alternatives-considered>Alternatives considered

No alternatives have been considered (yet).

On Thu, Jun 30, 2016 at 1:04 PM David Sweeris via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:
By itself, this would break switch statements, since they have to be exhaustive.

If anyone has any ideas about how to fix that, I'm all ears.

- Dave Sweeris

> On Jun 30, 2016, at 14:58, Edward Valentini via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:
>
>
> I am finding myself in a situation where the most elegant "swifty" solution would be to allow enum extensions to add to existing case options. For example lets say I'm using a library that has the following enum defined:
>
> enum MyDirection {
> case east, west
> }
>
> My app for example also makes use of north and south, so I would love to be able to write:
>
> extension MyDirection {
> case north,south
> }
>
> In objective c, one would probably have defined constants like MyDirectionEast etc... these would probably have been mapped to ints or strings so a consumer of this library could have easily extended this to add additional functionality, but using constants like that is not very "swifty"
>
> I'm curious what the swift community thinks.
>
> Thank you
> _______________________________________________
> swift-evolution mailing list
> swift-evolution@swift.org <mailto:swift-evolution@swift.org>
> https://lists.swift.org/mailman/listinfo/swift-evolution
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution
--
Dan Appel

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution

--
Dan Appel
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution


(Paul Cantrell) #14

That is the current workaround (as the proposal mentions), but it is still missing support for enum features such as associated values and the pattern matching power that they bring.

Fair enough, it would be hard to generalize pattern matching to this approach. Associated types are a whole other kettle of fish.

Also, by locking your OpenEnum conformers to reference types, you lose out on the value-semantics (very important, even for enums), and bring in the extra weight that a class is.

The class approach is the more lightweight option when you aren’t trying to get associated-value-like behavior.

There’s a fixed pool of instances, one per possible value, so there’s no per-usage allocation overhead. All one passes around are references to those fixed instances, so passing and comparing values is a one-word operation. The class is final, so any method dispatch is static.

Finally, you get the simplicity of leaning on pointer uniqueness to give you case uniqueness. Nothing to sneeze at there.

• • •

If you’re looking to have associated type-like behavior _and_ open cases, then yes, this “unique instances” approach breaks down.

At that point, though, why not just use a collection of separate struct types implementing a shared protocol?

    public protocol FileError: ErrorProtocol { }

    struct FileNotFound: FileError {
      let path: String
    }

    struct CorruptedFile {
      let bytes: [Int8]
    }

    func handleFileError(error: FileError) {
      switch(error) {
        case is CorruptedFile:
          print("Bummer")
        case let fileNotFound as FileNotFound:
          print("Can’t find \(fileNotFound.path)")
      }
    }

Here the dynamic type takes on the role of the enum value, and case let x as X gives you must of what associated types give.

Separate struct types are what I use for the problem the proposal mentions — structured, matchable errors with diagnostic data — and it does work out nicely in practice. Nicer, in fact; I’d say that this:

    if error is FileError { … }

…is easier to read and to remember than this:

    if case .fileError = error { … }

Cheers,

Paul

···

On Jun 30, 2016, at 3:54 PM, Dan Appel <dan.appel00@gmail.com> wrote:


(Dan Appel) #15

David,

Yeah, that's what I'm worried about. I was meaning to ask some engineers
about the implementation of this during WWDC (hence why I didn't send it
out), but didn't get a chance to do so.

···

On Thu, Jun 30, 2016 at 2:09 PM David Waite <david@alkaline-solutions.com> wrote:

On Jun 30, 2016, at 2:54 PM, Dan Appel via swift-evolution < > swift-evolution@swift.org> wrote:

Paul,

That is the current workaround (as the proposal mentions), but it is still
missing support for enum features such as associated values and the pattern
matching power that they bring.

I don’t believe a developer would be able to extend an enum to support
arbitrary associated values, the same as the limitation that one cannot
extend a type today to have extra members. Value types need to have an
understood size and structure at compile time of the file/module that they
are in.

Dan

On Thu, Jun 30, 2016 at 1:42 PM Paul Cantrell <cantrell@pobox.com> wrote:

While it doesn’t give all the “raw value” functionality of enum, it’s
possible to use object instance uniqueness to get enum-like behavior that
can be extended:

    public protocol OpenEnum: class, Hashable { }

    extension OpenEnum {
      public var hashValue: Int {
        return ObjectIdentifier(self).hashValue
      }
    }

    public func ==<T: OpenEnum>(lhs: T, rhs: T) -> Bool {
      return lhs === rhs
    }

A library can provide:

    public final class Color: OpenEnum, CustomStringConvertible {
      public let description: String

      public init(description: String) {
        self.description = description
      }

      static let
        black = Color(description: "black"),
        white = Color(description: "white")
    }

And then in a client project:

    extension Color {
      static let
        puce = Color(description: "puce"),
        mauve = Color(description: "mauve"),
        fuchsia = Color(description: "fuchsia")
    }

(This is how Siesta provides an extensible set of pipeline stages.
https://github.com/bustoutsolutions/siesta/pull/64)

With this approach, you still get the .member shortcut in some
circumstances:

    let eyebleedPalette: [Color] = [.fuchsia, .black, .mauve]

…but not in others:

    // Compiles
    switch(color) {
      case *Color*.red: print("Danger!")
      case *Color*.mauve: print("Dancing!")
      default: print("Nothing notable")
    }

    // Does not compile
    switch(color) {
      case .red: print("Danger!")
      case .mauve: print("Dancing!")
      default: print("Nothing notable")
    }

Given that this already comes close to giving the sort of functionality
one would want out of an extensible enum, perhaps it’s better to fill out
the gaps in this approach instead of adding a new language feature? This
would have the advantage of not adding a keyword, and presumably provide
useful behaviors that generalize to patterns other than extensible enums.

Cheers,

Paul

On Jun 30, 2016, at 3:23 PM, Guillermo Peralta Scura via swift-evolution < >> swift-evolution@swift.org> wrote:

I think the approach taken by your proporsal is really good. Would love
to have that feature for the language.

El jue., 30 jun. 2016 a las 16:19, Edward Valentini via swift-evolution (< >> swift-evolution@swift.org>) escribió:

I really like the idea of making it opt in with the extensible keyword
as opposed to opt out with final so this way there is no impact on existing
code

On Jun 30, 2016, at 16:15, Dan Appel <dan.appel00@gmail.com> wrote:

I've had a draft of a proposal lying around for a while which addresses
exactly this, but I haven't gotten around to sending it out for comments
yet. Link
<https://gist.github.com/Danappelxx/41b7c2e86787f75698bd48135cc616f5>.

Would appreciate if you guys took a look.
Dan Appel

Pasted inline below

Extensible Enums

   - Proposal: SE-NNNN
   <https://github.com/apple/swift-evolution/blob/master/proposals/NNNN-name.md>
   - Author: Dan Appel <https://github.com/danappelxx>
   - Status: Awaiting review
   <https://gist.github.com/Danappelxx/41b7c2e86787f75698bd48135cc616f5#rationale>
   - Review manager: TBD

<https://gist.github.com/Danappelxx/41b7c2e86787f75698bd48135cc616f5#introduction>
Introduction

This proposal introduces a new keyword that can be applied to enums
which allows new cases to be introduced in extensions.

Swift-evolution thread: [RFC] Extensible Enums
<https://lists.swift.org/pipermail/swift-evolution>

<https://gist.github.com/Danappelxx/41b7c2e86787f75698bd48135cc616f5#motivation>
Motivation

Enums are a powerful feature which provides a lot of benefit if you have
a limited number of behaviors. For example, associated values provide the
ability to make every case essentially a separate type. However, due to the
static nature of enums, they cannot be used in situations where they would
otherwise be a perfect fit.

An example of this would be the use of an Error enum like so:

enum FileError: ErrorProtocol {
    case fileNotFound(path: String)
    case corruptedFile(bytes: [Int8])
}func readFile() throws { ... }
// elsewhere in the codebasedo {
    try readFile()
} catch let error as FileError {
    switch error {
        case .fileNotFound(let path): // handle error
        case .corruptedFile(let bytes): // handle error
    }
} catch { ... }

While this is generally a good approach, it can be very dangerous for
library consumers if the author exposes the error to the user. This is due
to the fact that the switch statement has to be exhaustive and is only
satisfied when all enum cases have been accounted for. What this means for
library authors is that every time they add a new case to a public enum,
they are breaking the exhaustivity of the switch and making their
library backwards-incompatible.

Currently, the best workaround is to use a struct with static instances
and overloading the ~= operator. This allows for similar switch behavior
but overall is much less flexible, missing key features such as associated
values.

Another example is when the library is split into multiple modules,
where the error is defined in the first module and the second module wants
to add some error cases. An enum is very rarely used in this case because
you cannot add cases in other modules. Instead, library authors either use
an error protocol, and add more types that conform to it, or use the struct
approach shown above. While this is not terrible, adding cases in
extensions would better translate the intention of the author and adds more
flexiblity.

<https://gist.github.com/Danappelxx/41b7c2e86787f75698bd48135cc616f5#proposed-solution>Proposed
solution

The solution proposed is quite simple: add an extensible keyword/modifier
that can be applied to enums, which would require the default case when
switched on and allow new cases to be added in extensions.

Here is the translation of the very first example to the use an
extensible enum instead, with a new case added:

extensible enum ThingError: ErrorProtocol {
    case fileNotFound(path: String)
    case corruptedFile(bytes: [Int8])
    case failedReadingFile
}func readFile() throws { ... }
// elsewhere in the codebasedo {
    try readFile()
} catch let error as ThingError {
    switch error {
        case .fileNotFound(let path): // handle error
        case .corruptedFile(let bytes): // handle error
        default: // handle future errors that don't exist yet
    }
} catch { ... }

For the second example, we can simply extend the enum in the
higher-level module.

// Module FileProtocol

extensible enum FileError: ErrorProtocol {
    case fileNotFound(path: String)
}
protocol FileProtocol {
    func read() throws
}
// Module File
extension FileError {
    case corruptedFile(bytes: [Int8])
    case failedReadingFile
}
struct File: FileProtocol {
    func read() throws { ... }
}

<https://gist.github.com/Danappelxx/41b7c2e86787f75698bd48135cc616f5#detailed-design>Detailed
design

A new keyword would be added to the language which is only allowed in
front of the enum keyword. When an enum is marked extensible, new cases
can be added in extensions and switches that are performed on it require a
defaultcase.

<https://gist.github.com/Danappelxx/41b7c2e86787f75698bd48135cc616f5#impact-on-existing-code>Impact
on existing code

There is no impact on existing code since this is purely an additive
feature.

<https://gist.github.com/Danappelxx/41b7c2e86787f75698bd48135cc616f5#alternatives-considered>Alternatives
considered

No alternatives have been considered (yet).

On Thu, Jun 30, 2016 at 1:04 PM David Sweeris via swift-evolution < >>> swift-evolution@swift.org> wrote:

By itself, this would break switch statements, since they have to be
exhaustive.

If anyone has any ideas about how to fix that, I'm all ears.

- Dave Sweeris

> On Jun 30, 2016, at 14:58, Edward Valentini via swift-evolution < >>>> swift-evolution@swift.org> wrote:
>
>
> I am finding myself in a situation where the most elegant "swifty"
solution would be to allow enum extensions to add to existing case
options. For example lets say I'm using a library that has the following
enum defined:
>
> enum MyDirection {
> case east, west
> }
>
> My app for example also makes use of north and south, so I would love
to be able to write:
>
> extension MyDirection {
> case north,south
> }
>
> In objective c, one would probably have defined constants like
MyDirectionEast etc... these would probably have been mapped to ints or
strings so a consumer of this library could have easily extended this to
add additional functionality, but using constants like that is not very
"swifty"
>
> I'm curious what the swift community thinks.
>
> Thank you
> _______________________________________________
> swift-evolution mailing list
> swift-evolution@swift.org
> https://lists.swift.org/mailman/listinfo/swift-evolution
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

--
Dan Appel

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

--

Dan Appel
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

--

Dan Appel


(Dan Appel) #16

Paul,

Fair enough, it would be hard to generalize pattern matching to this

approach. Associated types are a whole other kettle of fish.

Right, but associated types are the main reason you would use this over
structs with static members.

The class approach is the more lightweight option when you aren’t trying

to get associated-value-like behavior.

You're right, that is true if the errors are static instances.

If you’re looking to have associated type-like behavior _and_ open cases,

then yes, this “unique instances” approach breaks down. At that point,
though, why not just use a collection of separate struct types implementing
a shared protocol?

Yes, as I mentioned in the draft, this is as close as you get to associated
values on enum cases. However, I think that enums better represent user
intent + have better language support. You can definitely emulate the
extensible feature using other language constructs, but after all you can
also emulate generics using Any (how java does it). In this case I think
its helpful to have first-class language support.

Dan

···

On Thu, Jun 30, 2016 at 2:27 PM Dan Appel <dan.appel00@gmail.com> wrote:

David,

Yeah, that's what I'm worried about. I was meaning to ask some engineers
about the implementation of this during WWDC (hence why I didn't send it
out), but didn't get a chance to do so.

On Thu, Jun 30, 2016 at 2:09 PM David Waite <david@alkaline-solutions.com> > wrote:

On Jun 30, 2016, at 2:54 PM, Dan Appel via swift-evolution < >> swift-evolution@swift.org> wrote:

Paul,

That is the current workaround (as the proposal mentions), but it is
still missing support for enum features such as associated values and the
pattern matching power that they bring.

I don’t believe a developer would be able to extend an enum to support
arbitrary associated values, the same as the limitation that one cannot
extend a type today to have extra members. Value types need to have an
understood size and structure at compile time of the file/module that they
are in.

Dan

On Thu, Jun 30, 2016 at 1:42 PM Paul Cantrell <cantrell@pobox.com> wrote:

While it doesn’t give all the “raw value” functionality of enum, it’s
possible to use object instance uniqueness to get enum-like behavior that
can be extended:

    public protocol OpenEnum: class, Hashable { }

    extension OpenEnum {
      public var hashValue: Int {
        return ObjectIdentifier(self).hashValue
      }
    }

    public func ==<T: OpenEnum>(lhs: T, rhs: T) -> Bool {
      return lhs === rhs
    }

A library can provide:

    public final class Color: OpenEnum, CustomStringConvertible {
      public let description: String

      public init(description: String) {
        self.description = description
      }

      static let
        black = Color(description: "black"),
        white = Color(description: "white")
    }

And then in a client project:

    extension Color {
      static let
        puce = Color(description: "puce"),
        mauve = Color(description: "mauve"),
        fuchsia = Color(description: "fuchsia")
    }

(This is how Siesta provides an extensible set of pipeline stages.
https://github.com/bustoutsolutions/siesta/pull/64)

With this approach, you still get the .member shortcut in some
circumstances:

    let eyebleedPalette: [Color] = [.fuchsia, .black, .mauve]

…but not in others:

    // Compiles
    switch(color) {
      case *Color*.red: print("Danger!")
      case *Color*.mauve: print("Dancing!")
      default: print("Nothing notable")
    }

    // Does not compile
    switch(color) {
      case .red: print("Danger!")
      case .mauve: print("Dancing!")
      default: print("Nothing notable")
    }

Given that this already comes close to giving the sort of functionality
one would want out of an extensible enum, perhaps it’s better to fill out
the gaps in this approach instead of adding a new language feature? This
would have the advantage of not adding a keyword, and presumably provide
useful behaviors that generalize to patterns other than extensible enums.

Cheers,

Paul

On Jun 30, 2016, at 3:23 PM, Guillermo Peralta Scura via swift-evolution >>> <swift-evolution@swift.org> wrote:

I think the approach taken by your proporsal is really good. Would love
to have that feature for the language.

El jue., 30 jun. 2016 a las 16:19, Edward Valentini via swift-evolution >>> (<swift-evolution@swift.org>) escribió:

I really like the idea of making it opt in with the extensible keyword
as opposed to opt out with final so this way there is no impact on existing
code

On Jun 30, 2016, at 16:15, Dan Appel <dan.appel00@gmail.com> wrote:

I've had a draft of a proposal lying around for a while which addresses
exactly this, but I haven't gotten around to sending it out for comments
yet. Link
<https://gist.github.com/Danappelxx/41b7c2e86787f75698bd48135cc616f5>.

Would appreciate if you guys took a look.
Dan Appel

Pasted inline below

Extensible Enums

   - Proposal: SE-NNNN
   <https://github.com/apple/swift-evolution/blob/master/proposals/NNNN-name.md>
   - Author: Dan Appel <https://github.com/danappelxx>
   - Status: Awaiting review
   <https://gist.github.com/Danappelxx/41b7c2e86787f75698bd48135cc616f5#rationale>
   - Review manager: TBD

<https://gist.github.com/Danappelxx/41b7c2e86787f75698bd48135cc616f5#introduction>
Introduction

This proposal introduces a new keyword that can be applied to enums
which allows new cases to be introduced in extensions.

Swift-evolution thread: [RFC] Extensible Enums
<https://lists.swift.org/pipermail/swift-evolution>

<https://gist.github.com/Danappelxx/41b7c2e86787f75698bd48135cc616f5#motivation>
Motivation

Enums are a powerful feature which provides a lot of benefit if you
have a limited number of behaviors. For example, associated values provide
the ability to make every case essentially a separate type. However, due to
the static nature of enums, they cannot be used in situations where they
would otherwise be a perfect fit.

An example of this would be the use of an Error enum like so:

enum FileError: ErrorProtocol {
    case fileNotFound(path: String)
    case corruptedFile(bytes: [Int8])
}func readFile() throws { ... }
// elsewhere in the codebasedo {
    try readFile()
} catch let error as FileError {
    switch error {
        case .fileNotFound(let path): // handle error
        case .corruptedFile(let bytes): // handle error
    }
} catch { ... }

While this is generally a good approach, it can be very dangerous for
library consumers if the author exposes the error to the user. This is due
to the fact that the switch statement has to be exhaustive and is only
satisfied when all enum cases have been accounted for. What this means for
library authors is that every time they add a new case to a public enum,
they are breaking the exhaustivity of the switch and making their
library backwards-incompatible.

Currently, the best workaround is to use a struct with static
instances and overloading the ~= operator. This allows for similar
switch behavior but overall is much less flexible, missing key
features such as associated values.

Another example is when the library is split into multiple modules,
where the error is defined in the first module and the second module wants
to add some error cases. An enum is very rarely used in this case because
you cannot add cases in other modules. Instead, library authors either use
an error protocol, and add more types that conform to it, or use the struct
approach shown above. While this is not terrible, adding cases in
extensions would better translate the intention of the author and adds more
flexiblity.

<https://gist.github.com/Danappelxx/41b7c2e86787f75698bd48135cc616f5#proposed-solution>Proposed
solution

The solution proposed is quite simple: add an extensible keyword/modifier
that can be applied to enums, which would require the default case
when switched on and allow new cases to be added in extensions.

Here is the translation of the very first example to the use an
extensible enum instead, with a new case added:

extensible enum ThingError: ErrorProtocol {
    case fileNotFound(path: String)
    case corruptedFile(bytes: [Int8])
    case failedReadingFile
}func readFile() throws { ... }
// elsewhere in the codebasedo {
    try readFile()
} catch let error as ThingError {
    switch error {
        case .fileNotFound(let path): // handle error
        case .corruptedFile(let bytes): // handle error
        default: // handle future errors that don't exist yet
    }
} catch { ... }

For the second example, we can simply extend the enum in the
higher-level module.

// Module FileProtocol

extensible enum FileError: ErrorProtocol {
    case fileNotFound(path: String)
}
protocol FileProtocol {
    func read() throws
}
// Module File
extension FileError {
    case corruptedFile(bytes: [Int8])
    case failedReadingFile
}
struct File: FileProtocol {
    func read() throws { ... }
}

<https://gist.github.com/Danappelxx/41b7c2e86787f75698bd48135cc616f5#detailed-design>Detailed
design

A new keyword would be added to the language which is only allowed in
front of the enum keyword. When an enum is marked extensible, new
cases can be added in extensions and switches that are performed on it
require a defaultcase.

<https://gist.github.com/Danappelxx/41b7c2e86787f75698bd48135cc616f5#impact-on-existing-code>Impact
on existing code

There is no impact on existing code since this is purely an additive
feature.

<https://gist.github.com/Danappelxx/41b7c2e86787f75698bd48135cc616f5#alternatives-considered>Alternatives
considered

No alternatives have been considered (yet).

On Thu, Jun 30, 2016 at 1:04 PM David Sweeris via swift-evolution < >>>> swift-evolution@swift.org> wrote:

By itself, this would break switch statements, since they have to be
exhaustive.

If anyone has any ideas about how to fix that, I'm all ears.

- Dave Sweeris

> On Jun 30, 2016, at 14:58, Edward Valentini via swift-evolution < >>>>> swift-evolution@swift.org> wrote:
>
>
> I am finding myself in a situation where the most elegant "swifty"
solution would be to allow enum extensions to add to existing case
options. For example lets say I'm using a library that has the following
enum defined:
>
> enum MyDirection {
> case east, west
> }
>
> My app for example also makes use of north and south, so I would
love to be able to write:
>
> extension MyDirection {
> case north,south
> }
>
> In objective c, one would probably have defined constants like
MyDirectionEast etc... these would probably have been mapped to ints or
strings so a consumer of this library could have easily extended this to
add additional functionality, but using constants like that is not very
"swifty"
>
> I'm curious what the swift community thinks.
>
> Thank you
> _______________________________________________
> swift-evolution mailing list
> swift-evolution@swift.org
> https://lists.swift.org/mailman/listinfo/swift-evolution
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

--
Dan Appel

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

--

Dan Appel
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

--

Dan Appel

--
Dan Appel


(Marc Palmer) #17

Hi,

I too groan when faced with the lack of extensibility on enums. As a potential framework writer, I'd like to be able to use an enum as a key to dictionaries, supplying a minimum set of such enum cases, but allowing app developers to add new ones they require.

Having read the proposal however, I have a major concern and question the entire idea.

Given that there is unlikely to be a sane way to order the extended enum cases supplied by other modules, we will never be able to rely on the automatic ordinal values applied, nor their relative position in the natural sequence, for there isn't one outside of the first set of cases in the original definition.

For many cases this may be fine, on the understanding that everything would have to compile from source, but my understanding is that we don't want that in future with ABI around the corner. Binary libraries would probably need to bake in the value of e.g. Int enum cases. (I think?)

I fear that if this proposal were implemented without some major restrictions (such as never allowing use of rawValue), we would regret it and suffer for example having to explicitly set enum case Int raw values for every case in these enums in every module always, and suffer compilation errors when other (maybe binary) modules change their explicit raw values and clash with other modules. It could be a dependency nightmare.

Essentially consigning extensible enums to never being useful for serialising their raw values seems of limited use to me, as often you may not know you need them to have unmoving raw values until it is too late and your code is in the wild.

Perhaps I am missing some secret sauce?

···

--
Marc Palmer


(Paul Cantrell) #18

Where I’m going with my line of though is:

1. Extensible enums probably aren’t going to happen, for reasons mentioned earlier.

2. I don’t really see the protocol+structs approach as “emulating enums” at all. IMO, separate types are a more accurate model of what’s going on with the example you gave. (The computed properties insight is a clue that they’re different types.)

…BUT…

3. If there is indeed a situation where the protocol+structs approach falls short, then we should identify it, because there might be a good language proposal in it.

Cheers, P

···

On Jun 30, 2016, at 5:32 PM, Dan Appel <dan.appel00@gmail.com> wrote:

Paul,

I should have came with an example where enums with associated types beat out a protocol-oriented approach, but I can't think of any off the top of my head. My gut tells me that trying to emulate enums through other constructs to get more functionality means that the language has failed you, but maybe this is not one of those cases.

Really I just want to use enums as much as possible, and I enjoy the first-class support they get in the Swift language (especially compared to other languages). If I can use an enum instead of a struct or class, I almost always will, and I'm just trying to fix one of the cases where its simply not possible due to language limitations.

Dan

On Thu, Jun 30, 2016 at 3:14 PM Paul Cantrell <cantrell@pobox.com <mailto:cantrell@pobox.com>> wrote:

On Jun 30, 2016, at 4:43 PM, Dan Appel <dan.appel00@gmail.com <mailto:dan.appel00@gmail.com>> wrote:

If you’re looking to have associated type-like behavior _and_ open cases, then yes, this “unique instances” approach breaks down. At that point, though, why not just use a collection of separate struct types implementing a shared protocol?

Yes, as I mentioned in the draft, this is as close as you get to associated values on enum cases. However, I think that enums better represent user intent + have better language support.

You could make the case that they better represent intent. Not totally sold on that, but I could see the argument.

What’s an example of “better language support?” Is there a specific situation where this approach doesn’t work well, but would if it were instead an enum with associated types?

    public protocol FileError: ErrorProtocol { }

    struct FileNotFound: FileError {
      let path: String
    }

    struct CorruptedFile {
      let bytes: [Int8]
    }

The guarantee of exhaustive case matching is the only big difference I can think of. Remove that, and there’s not much difference in practice with this:

    func handleFileError(error: FileError) {
      switch(error) {
        case is CorruptedFile:
          print("Bummer")
        case let error as FileNotFound:
          print("Can’t find \(error.path)")
        default:
          break
      }

…vs this:

    func handleFileError(error: FileError) {
      switch(error) {
        case .corruptedFile:
          print("Bummer")
        case .fileNotFound(let path):
          print("Can’t find \(path)")
        default:
          break
      }
    }

But maybe there’s a situation I’m missing where things really would be much easier if it were an enum?

You can definitely emulate the extensible feature using other language constructs, but after all you can also emulate generics using Any (how java does it).

That’s not a good analogy. Leave aside “emulate generics using Any” is not really a good description of Java’s compile-time-only generic types. In the case of using different struct types for error, you’re not sacrificing any sort of compile-time safety over enums with associated types, whereas [Any] everywhere inevitably involves unsafe casting.

Cheers, P

In this case I think its helpful to have first-class language support.

Dan

On Thu, Jun 30, 2016 at 2:27 PM Dan Appel <dan.appel00@gmail.com <mailto:dan.appel00@gmail.com>> wrote:
David,

Yeah, that's what I'm worried about. I was meaning to ask some engineers about the implementation of this during WWDC (hence why I didn't send it out), but didn't get a chance to do so.

On Thu, Jun 30, 2016 at 2:09 PM David Waite <david@alkaline-solutions.com <mailto:david@alkaline-solutions.com>> wrote:

On Jun 30, 2016, at 2:54 PM, Dan Appel via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

Paul,

That is the current workaround (as the proposal mentions), but it is still missing support for enum features such as associated values and the pattern matching power that they bring.

I don’t believe a developer would be able to extend an enum to support arbitrary associated values, the same as the limitation that one cannot extend a type today to have extra members. Value types need to have an understood size and structure at compile time of the file/module that they are in.

Dan

On Thu, Jun 30, 2016 at 1:42 PM Paul Cantrell <cantrell@pobox.com <mailto:cantrell@pobox.com>> wrote:
While it doesn’t give all the “raw value” functionality of enum, it’s possible to use object instance uniqueness to get enum-like behavior that can be extended:

    public protocol OpenEnum: class, Hashable { }

    extension OpenEnum {
      public var hashValue: Int {
        return ObjectIdentifier(self).hashValue
      }
    }

    public func ==<T: OpenEnum>(lhs: T, rhs: T) -> Bool {
      return lhs === rhs
    }

A library can provide:

    public final class Color: OpenEnum, CustomStringConvertible {
      public let description: String

      public init(description: String) {
        self.description = description
      }

      static let
        black = Color(description: "black"),
        white = Color(description: "white")
    }

And then in a client project:

    extension Color {
      static let
        puce = Color(description: "puce"),
        mauve = Color(description: "mauve"),
        fuchsia = Color(description: "fuchsia")
    }

(This is how Siesta provides an extensible set of pipeline stages. https://github.com/bustoutsolutions/siesta/pull/64)

With this approach, you still get the .member shortcut in some circumstances:

    let eyebleedPalette: [Color] = [.fuchsia, .black, .mauve]

…but not in others:

    // Compiles
    switch(color) {
      case Color.red: print("Danger!")
      case Color.mauve: print("Dancing!")
      default: print("Nothing notable")
    }

    // Does not compile
    switch(color) {
      case .red: print("Danger!")
      case .mauve: print("Dancing!")
      default: print("Nothing notable")
    }

Given that this already comes close to giving the sort of functionality one would want out of an extensible enum, perhaps it’s better to fill out the gaps in this approach instead of adding a new language feature? This would have the advantage of not adding a keyword, and presumably provide useful behaviors that generalize to patterns other than extensible enums.

Cheers,

Paul

On Jun 30, 2016, at 3:23 PM, Guillermo Peralta Scura via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

I think the approach taken by your proporsal is really good. Would love to have that feature for the language.

El jue., 30 jun. 2016 a las 16:19, Edward Valentini via swift-evolution (<swift-evolution@swift.org <mailto:swift-evolution@swift.org>>) escribió:

I really like the idea of making it opt in with the extensible keyword as opposed to opt out with final so this way there is no impact on existing code

On Jun 30, 2016, at 16:15, Dan Appel <dan.appel00@gmail.com <mailto:dan.appel00@gmail.com>> wrote:

I've had a draft of a proposal lying around for a while which addresses exactly this, but I haven't gotten around to sending it out for comments yet. Link <https://gist.github.com/Danappelxx/41b7c2e86787f75698bd48135cc616f5>.

Would appreciate if you guys took a look.
Dan Appel

Pasted inline below

Extensible Enums

Proposal: SE-NNNN <https://github.com/apple/swift-evolution/blob/master/proposals/NNNN-name.md>
Author: Dan Appel <https://github.com/danappelxx>
Status: Awaiting review <https://gist.github.com/Danappelxx/41b7c2e86787f75698bd48135cc616f5#rationale>
Review manager: TBD
<https://gist.github.com/Danappelxx/41b7c2e86787f75698bd48135cc616f5#introduction>Introduction

This proposal introduces a new keyword that can be applied to enums which allows new cases to be introduced in extensions.

Swift-evolution thread: [RFC] Extensible Enums <https://lists.swift.org/pipermail/swift-evolution>
<https://gist.github.com/Danappelxx/41b7c2e86787f75698bd48135cc616f5#motivation>Motivation

Enums are a powerful feature which provides a lot of benefit if you have a limited number of behaviors. For example, associated values provide the ability to make every case essentially a separate type. However, due to the static nature of enums, they cannot be used in situations where they would otherwise be a perfect fit.

An example of this would be the use of an Error enum like so:

enum FileError: ErrorProtocol {
    case fileNotFound(path: String)
    case corruptedFile(bytes: [Int8])
}
func readFile() throws { ... }

// elsewhere in the codebase
do {
    try readFile()
} catch let error as FileError {
    switch error {
        case .fileNotFound(let path): // handle error
        case .corruptedFile(let bytes): // handle error
    }
} catch { ... }
While this is generally a good approach, it can be very dangerous for library consumers if the author exposes the error to the user. This is due to the fact that the switch statement has to be exhaustive and is only satisfied when all enum cases have been accounted for. What this means for library authors is that every time they add a new case to a public enum, they are breaking the exhaustivity of the switch and making their library backwards-incompatible.

Currently, the best workaround is to use a struct with static instances and overloading the ~= operator. This allows for similar switch behavior but overall is much less flexible, missing key features such as associated values.

Another example is when the library is split into multiple modules, where the error is defined in the first module and the second module wants to add some error cases. An enum is very rarely used in this case because you cannot add cases in other modules. Instead, library authors either use an error protocol, and add more types that conform to it, or use the struct approach shown above. While this is not terrible, adding cases in extensions would better translate the intention of the author and adds more flexiblity.

<https://gist.github.com/Danappelxx/41b7c2e86787f75698bd48135cc616f5#proposed-solution>Proposed solution

The solution proposed is quite simple: add an extensible keyword/modifier that can be applied to enums, which would require the default case when switched on and allow new cases to be added in extensions.

Here is the translation of the very first example to the use an extensible enum instead, with a new case added:

extensible enum ThingError: ErrorProtocol {
    case fileNotFound(path: String)
    case corruptedFile(bytes: [Int8])
    case failedReadingFile
}
func readFile() throws { ... }

// elsewhere in the codebase
do {
    try readFile()
} catch let error as ThingError {
    switch error {
        case .fileNotFound(let path): // handle error
        case .corruptedFile(let bytes): // handle error
        default: // handle future errors that don't exist yet
    }
} catch { ... }
For the second example, we can simply extend the enum in the higher-level module.

// Module FileProtocol

extensible enum FileError: ErrorProtocol {
    case fileNotFound(path: String)
}

protocol FileProtocol {
    func read() throws
}

// Module File

extension FileError {
    case corruptedFile(bytes: [Int8])
    case failedReadingFile
}

struct File: FileProtocol {
    func read() throws { ... }
}
<https://gist.github.com/Danappelxx/41b7c2e86787f75698bd48135cc616f5#detailed-design>Detailed design

A new keyword would be added to the language which is only allowed in front of the enum keyword. When an enum is marked extensible, new cases can be added in extensions and switches that are performed on it require a defaultcase.

<https://gist.github.com/Danappelxx/41b7c2e86787f75698bd48135cc616f5#impact-on-existing-code>Impact on existing code

There is no impact on existing code since this is purely an additive feature.

<https://gist.github.com/Danappelxx/41b7c2e86787f75698bd48135cc616f5#alternatives-considered>Alternatives considered

No alternatives have been considered (yet).

On Thu, Jun 30, 2016 at 1:04 PM David Sweeris via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:
By itself, this would break switch statements, since they have to be exhaustive.

If anyone has any ideas about how to fix that, I'm all ears.

- Dave Sweeris

> On Jun 30, 2016, at 14:58, Edward Valentini via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:
>
>
> I am finding myself in a situation where the most elegant "swifty" solution would be to allow enum extensions to add to existing case options. For example lets say I'm using a library that has the following enum defined:
>
> enum MyDirection {
> case east, west
> }
>
> My app for example also makes use of north and south, so I would love to be able to write:
>
> extension MyDirection {
> case north,south
> }
>
> In objective c, one would probably have defined constants like MyDirectionEast etc... these would probably have been mapped to ints or strings so a consumer of this library could have easily extended this to add additional functionality, but using constants like that is not very "swifty"
>
> I'm curious what the swift community thinks.
>
> Thank you
> _______________________________________________
> swift-evolution mailing list
> swift-evolution@swift.org <mailto:swift-evolution@swift.org>
> https://lists.swift.org/mailman/listinfo/swift-evolution
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution
--
Dan Appel

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution

--
Dan Appel
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution

--
Dan Appel
--
Dan Appel

--
Dan Appel


(Andrew Bennett) #19

How many of these use cases would be safely addressed if two Enum types
could be unioned to form a new type?

It could use syntax similar to what is being proposed for existentials:
(A|B), or something like this:

enum C: A, B {}

Swift could generate code like this:

enum A {
  case A1, A2
}
enum B {
  case B1, B2
}
enum C {
  case A(Module.A)
  case B(Module.B)

  init(_ a: Module.A) { self = .A(a) }
  init(_ b: Module.B) { self = .B(b) }

  static let A1 = C(A.A1)
  static let A2 = C(A.A2)
  static let B1 = C(B.B1)
  static let B2 = C(B.B2)
}
extension A {
  init?(_ c: C) {
    guard let case .A(a) = c else { return nil }
    self = a
  }
}
extension B {
  init?(_ c: C) {
    guard let case .B(b) = c else { return nil }
    self = b
  }
}

If I remember correctly there was already some proposals like this, they
are probably more thought out than this suggestion. I know I'd find that
useful, I don't think I'd want the exhaustibility implications of extending
an enum in another module.

···

On Friday, 1 July 2016, Marc Palmer via swift-evolution < swift-evolution@swift.org> wrote:

Hi,

I too groan when faced with the lack of extensibility on enums. As a
potential framework writer, I'd like to be able to use an enum as a key to
dictionaries, supplying a minimum set of such enum cases, but allowing app
developers to add new ones they require.

Having read the proposal however, I have a major concern and question the
entire idea.

Given that there is unlikely to be a sane way to order the extended enum
cases supplied by other modules, we will never be able to rely on the
automatic ordinal values applied, nor their relative position in the
natural sequence, for there isn't one outside of the first set of cases in
the original definition.

For many cases this may be fine, on the understanding that everything
would have to compile from source, but my understanding is that we don't
want that in future with ABI around the corner. Binary libraries would
probably need to bake in the value of e.g. Int enum cases. (I think?)

I fear that if this proposal were implemented without some major
restrictions (such as never allowing use of rawValue), we would regret it
and suffer for example having to explicitly set enum case Int raw values
for every case in these enums in every module always, and suffer
compilation errors when other (maybe binary) modules change their explicit
raw values and clash with other modules. It could be a dependency nightmare.

Essentially consigning extensible enums to never being useful for
serialising their raw values seems of limited use to me, as often you may
not know you need them to have unmoving raw values until it is too late and
your code is in the wild.

Perhaps I am missing some secret sauce?

--
Marc Palmer


(Austin Zheng) #20

Unions are a no-go.

https://github.com/apple/swift-evolution/blob/master/commonly_proposed.md

···

On Jun 30, 2016, at 5:00 PM, Andrew Bennett via swift-evolution <swift-evolution@swift.org> wrote:

How many of these use cases would be safely addressed if two Enum types could be unioned to form a new type?

It could use syntax similar to what is being proposed for existentials: (A|B), or something like this:

enum C: A, B {}

Swift could generate code like this:

enum A {
  case A1, A2
}
enum B {
  case B1, B2
}
enum C {
  case A(Module.A)
  case B(Module.B)

  init(_ a: Module.A) { self = .A(a) }
  init(_ b: Module.B) { self = .B(b) }

  static let A1 = C(A.A1)
  static let A2 = C(A.A2)
  static let B1 = C(B.B1)
  static let B2 = C(B.B2)
}
extension A {
  init?(_ c: C) {
    guard let case .A(a) = c else { return nil }
    self = a
  }
}
extension B {
  init?(_ c: C) {
    guard let case .B(b) = c else { return nil }
    self = b
  }
}

If I remember correctly there was already some proposals like this, they are probably more thought out than this suggestion. I know I'd find that useful, I don't think I'd want the exhaustibility implications of extending an enum in another module.

On Friday, 1 July 2016, Marc Palmer via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:
Hi,

I too groan when faced with the lack of extensibility on enums. As a potential framework writer, I'd like to be able to use an enum as a key to dictionaries, supplying a minimum set of such enum cases, but allowing app developers to add new ones they require.

Having read the proposal however, I have a major concern and question the entire idea.

Given that there is unlikely to be a sane way to order the extended enum cases supplied by other modules, we will never be able to rely on the automatic ordinal values applied, nor their relative position in the natural sequence, for there isn't one outside of the first set of cases in the original definition.

For many cases this may be fine, on the understanding that everything would have to compile from source, but my understanding is that we don't want that in future with ABI around the corner. Binary libraries would probably need to bake in the value of e.g. Int enum cases. (I think?)

I fear that if this proposal were implemented without some major restrictions (such as never allowing use of rawValue), we would regret it and suffer for example having to explicitly set enum case Int raw values for every case in these enums in every module always, and suffer compilation errors when other (maybe binary) modules change their explicit raw values and clash with other modules. It could be a dependency nightmare.

Essentially consigning extensible enums to never being useful for serialising their raw values seems of limited use to me, as often you may not know you need them to have unmoving raw values until it is too late and your code is in the wild.

Perhaps I am missing some secret sauce?

--
Marc Palmer

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution