SE-0280: Enum cases as protocol witnesses

I am not sure why we'd want to cater to users who find consistency to be confusing.

People are welcome to leave whatever feedback they like in review threads; you don't need to challenge them about it.

15 Likes

I didn't want to write another post about this proposal, but it looks like a discussion from the pitch is repeating, so I guess it doesn't hurt to add a short upshot.

I'm still convinced the positive aspects are not nearly as big as some people suggest, and there might still be some expectations this addition simply can't fulfill.
You loose a main feature of enums when you "hide" them behind a protocol: Depending on the exact setup, you may still be able to use switch, but the guarantees of exhaustiveness are gone.
It might be possible that those could be preserved, but I guess an idea like Enum cases as protocol witnesses might interfere with SE-0280.

Although some examples from the pitch discussion turned out to be not that persuasive, @hisekaldma finally brought up a true case where the proposal would definitely be helpful (Enum cases as protocol witnesses - imho that should be included in the proposal if it is not there yet).

While there are many who welcome SE-0280 as a simplification, it is not removing exceptions from the compiler code, but rather adds some lines — but afair, the bigger part is new tests, and it's not a huge change.
However, this feature will also need proper documentation, and I don't think it will simplify the chapter about protocols...

I appreciate that this proposal has some context (Protocol Witness Matching Mini-Manifesto), and I hope this is taken into account:
The more "duck typing" is added to Swift, the more people will get used to it, so the impact of this proposal might be bigger as it seems on first sight. Coming from Objective-C, I don't fear duck-typing - but I think this is at least a slight change of Swifts character.

Overall, I don't think I would benefit from SE-0280 — but while I wish for better acceptance of critique, I don't share the strong dislike of the few opposers: Many of those who don't need the feature might never notice it.

2 Likes

Sure, you can do it this way. But why should you have to? It's pure boilerplate, it prevents retroactive conformance (if JSONDecodingError is in another module, you can't change its definition to make room for the static members), and it interferes with pattern-matching on the error—for instance, you'd have to write:

do { ... }
catch JSONDecodingError._keyNotFound(let key) {
  ...
}

Why is it better that the compiler makes you jump through hoops to conform JSONDecodingError to DecodingError? The only reason I can think of is that you might think it's confusing that static var fileCorrupted: Self matches case fileCorrupted, but I think it will be reasonably clear in practice.

2 Likes

FWIW, I don't think this rules that out. If we added such a feature we'd end up with the following situation:

Implementation static var { get } static var { get set } case
static var Y Y
class var Y Y
static let Y
case Y Y

...which is a little more complex, but not unworkable. I do think it's fair to say that either proposal on its own would be less complex than both together, though.

2 Likes

I might be wrong, asking this question out of curiosity. Would static var { get set } match case if we'd allow enum cases with associated value to mutate their payload in-place? I remember this interesting case that would require such mutation to be the optimal solution.

Still no, since the mutating happens on an instance and therefore isn't static! But we're getting away from SE-0280 here.

4 Likes

In my view enums are separete entities and serve other purpose than classes/structs. What proposal introduces seems odd to me.

1 Like

A static function which returns Self is a factory or constructor method. Enum case names are case constructors. It is inconsistent when a protocol calls for a factory method if case constructors can't satisfy the requirement. I don't know why the fact that structs/classes are product types and enums are sum types should matter to protocol conformance.

But this was hashed and rehashed in the pitch thread, so that's enough from me.

4 Likes

I certainly agree with this. Enums and struct serve different purposes.

At least generally. It is sometimes not as clear cut. Bool is a struct but can only ever have two distinct values. And Optional is an enum, but can represent every other thinkable value, far too many to enumerate.

Enums can already conform to protocols. And enums have case constructors on the type that can be used to create instances. Struct can also have static constructors. These can be expressed as a protocols with (static) type requirements. Why aren't these composable? I simply don't understand this limitation. It seems inconsistent to me.

I think that if something looks like a duck, swims like a duck, and quacks like a duck, then it probably should be able to conform to the Duck protocol.

3 Likes

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

+1. In my understanding, an enum in Swift is a struct that can't hold stored properties and has special syntax for its static members, so if a struct can fulfil protocols with static members, enums should likewise be able to do so with their cases. I haven't run into this limitation before, but I agree that it patches an inconsistency in the language that I hadn't otherwise considered before.

Developers in other languages have expressed interest in Swift after I've talked about what enums can do. It's a selling point for the language and I think this could add to it.

2 Likes

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
Terms of Service

Privacy Policy

Cookie Policy