SE-0280: Enum cases as protocol witnesses

Fundamentally, classes and structs are records. Instances of either hold some data.

Enums are not fundamentally records - they are tagged unions of records.

That being said, you can impose some meaning to the elements of an enumā€™s payload to make a quasi-record type. Some might say thatā€™s a misuse of the feature, and in general Swift doesnā€™t support it very well (e.g. you canā€™t easily mutate those individual elements in-place, like you can with a class/struct).

Beacuse it is not an ordinary conformance. It is more about constructors as you put, which imo has little to do with var\funcs and more with inits.

ps
If it matters, I have seen some feedback that uses a pattern that allows to provide a type with default value aka null state, but imo this is a job for constructors without parameters. So it should be more like this

protocol HasDefaultConstructor {
    init()
}
protocol DefaultStateProvider: HasDefaultConstructor {
   static var defaultState: Self { get }
}
enum Mood: DefaultStateProvider {
    case good, bad, neutral
    init() { self = .neutral }
    static var defaultState: Mood { self.init() }
}
let thisProposalMadeMeFell_ = Mood()

Besides that the presence of default case in enum is actuallly nonsence based on what I have this in this thread, treating cases as var/func witneses which are in fact semanticaly behave as inits seems like an introduction of intentional confusion.
-1 again.

I think there is a difference - in your example, defaultState is just an "alias" for .neutral whereas:

protocol DefaultStateProvider {
   static var defaultState: Self { get }
}

enum Mood: DefaultStateProvider {
  case good, bad, neutral
  case defaultState
}

here, defaultState is itself a case and not just an "alias" for another case. This is what @sveinhal talked about earlier.

Although depending on the use-case, either would work as a "default" value.

Normal protocols can currently impose requirements on conforming types that they provide initialisers (such as Decodable and the various ā€¦Representable and ā€¦Convertible types).

It can also require that conforming types provide static vars that produce an instance of self with certain properties, such as floating pointsā€™ .greatestFiniteMagnitude or .zero etc.

Whether or not this feature of protocols is useful to all, or not, is beyond the point. It is already possible today, and types use these features.

It is also the case that at call sites, there is no syntactic or semantic difference between an enum case and static var/func. You cannot tell without inspecting types, wether the following parameter is an enum, an option set or just a default instance of some complex type:

let result = someCalculation(roundingTowards: .zero)

Here, .zero can be a number, a case of RoundingRule, a single-element of RoundingOptions, or maybe even a common shared instance of some much more complex type.

Point is; at call site, a case and a static var are identical. Combined with the fact that protocols can meaningfully express requirements on types that allow for such uses.

But you cannot today, write generic code over types that are for all intents and purposes semantically identical.

You can sometimes force an enum to conform, by providing a case alias through a static var indirection, but for already conforming types, you canā€™t simply express retroactive conformance. In fact, it is impossible to make it conform without renaming cases. The latter may not always be desirable when the type is mapping some external network response or whatever.

4 Likes

Just found an interesting case through a different forums thread where a library expects the user to use an enum constructor in a place where the library APIā€˜s signature awaits a function, which already works.

That said, if manual enum constructor forwarding as a function that returns Self works, then not having the protocol witness to match this behavior simply feels like a bug.

4 Likes

+1. It just makes logical sense to me.

2 Likes

+1 for consistency.
Followed the pitch thread & discussion

1 Like

+1. I've needed this in the past and had to work around it, so it would be great to remove this inconsistency in the language.

I can't remember the exact use case (it was a couple years ago that I tried to do this on a personal project that went nowhere), but this would be useful to create generic algorithms around protocols that represent a "policy" pattern:

  • The static variables/methods of the protocol define a finite set of "levels" or "categories" of a policy.
  • The protocol also has instance variables/methods that operate on data based on the policy level represented by the receiver.
  • Concrete types conform to the protocol and provide appropriate implementations of the policy. Since the protocol declares a finite set of policy levels, an enum is a natural representation, but it doesn't have to be.
  • Other algorithms use a generic argument constrained by the protocol, so they can request an instance of the policy by name and then operate on it.
1 Like

One case where I've needed this is when I need to add generic constraints on optionals. In order to do this, I have to define my own OptionalType protocol:

protocol OptionalType {
    associatedtype Wrapped
    ...
}
extension Optional: OptionalType { }

This lets me do:

extension SomeGenericThing where AGenericParameter: OptionalType { ... }

The big thing I've been missing is a way to use existing Optional cases in order to construct new values inside these generic extensions without defining even more things on the OptionalType protocol. With this proposal, I'll finally be able to do:

protocol OptionalType {
    associatedtype Wrapped
    static var none: Self { get }
    static func some(_ newValue: Wrapped) -> Self
}

And it'll all "just work".

+1000000

10 Likes

+ 0.5

I agree this is better than what we have today but I think it's unfortunate that it doesn't really add "protocol for enum" where we should be able to implement a default method by protocol extension like below.

protocol FooBar { ... }
enum RedFooBar: FooBar { case foo(Int), bar(Int) }
enum BlueFooBar: FooBar { case foo(Int), bar(Int) }
// the above will be possible is great.
// but what I really love to see is the below.
extension FooBar {
  var number: Int {
    switch self {
    case .foo(let n): return n
    case .bar(let n): return n * 42
    }
  }
}

At first I really liked the proposal, but the more I think about it, the more I see it as a bad trade-off.
I realize the problem, swift needs an interfacing pattern for enum cases, but I don't see enums as a true subset of static var/func. They behave identical in most use-cases, but there is a major additional interaction. Iterating though all cases. And this needs to be preserved for an interface of enums.
Additionally case and static var : Self, static func() -> Self is a different syntax. I think the mapping is too implicit, it would confuse beginners.
What if there would be a specialized protocol, that can inherit from the usual procol, something like that:

protocol Foo {
    static func other(Int) -> Self
}

enumProtocol Bar: Foo {
    case .one
    case .two
    case .other(Int)
}
enum RedBar: FooBar { case one, two, other(Int) }
enum BlueBar: FooBar { case one, two, special(Int), other(Int) }

extension Bar {
  var number: Int {
    switch self {
    case .one: return 1
    case .two: return 2
    case .other(let n): return n
    @unknown default: return 0
    }
  }
}

That would also allow what @nh7a suggests, which I believe is very useful.

1 Like

enumProtocol doesn't make much sense. Why would you ever want multiple enums that have exactly the same cases, instead of one enum?

@cukr
Because you sometimes want a more specialized enum and don't need all the additional cases in every use-case.

let x = BlueBar.special(123)
print(x.number)

What will be printed here? enumProtocol Bar doesn't know about that case

I was about to edit this part, it needs a fallback since it can't be exhaustive. I took @nh7a example as a template, it probably wasnā€™t the best choice.
It was just the first shot. Introducing a new keyword is maybe too big.

Seems like your enumProtocol failed to do that.

In a fully exhaustive way, yes. But you can iterate through all known cases.
Like I said it is only a thought experiment and not an elaborated proposal.

If you'd like to pitch enum protocol, please do so in another thread; it is off-topic here. Even if there were such a feature, it wouldn't obviate the question raised by this proposal.

4 Likes

Proposal Accepted

Please see the acceptance post in the Announcements section.

3 Likes