SE-0280: Enum cases as protocol witnesses

Sorry, I don't understand this bit. What does this proposal change with regards to Codable?

1 Like

Consider the following JSON files:

{ "foo": "a" }

{ "bar": "b" }

They can be parsed easily as the following struct type:

struct FooOrBar: Codable {
    let foo: String?
    let bar: String?
}

They can be parsed as a enum type, but with much more effort:

enum FooOrBar: Codable {
    case foo(String)
    case bar(String)
    
    private enum CodingKeys: String, CodingKey {
        case foo
        case bar
    }
    
    init(from decoder: Decoder) throws {
        // implementation omitted...
    }
    
    func encode(to encoder: Encoder) throws {
        // implementation omitted...
    }
}

I might have misunderstood the impact of this proposal, but it seems like that with enum cases as protocol witnesses, parsing the above JSON into a enum types will be as simple as if it were a struct type?

enum FooOrBar: Codable {
    case foo(String)
    case bar(String)
}

The proposal is for enum cases to match protocol requirements that are static functions or properties with the same signature. That's not really related, since foo and bar aren't requirements of Encodable or Decodable.

4 Likes

Having watched this proposal since its initial appearance, I still cannot see how the intended addition of functionality brings anything but confusion because it introduces division in code idioms. I have tried to digest it and tried to look for examples of what it potentially solves, but even in the official proposal, I did not found reasonable examples, so that in my opinion does not justify the presence of special behavior.

enum JSONDecodingError: DecodingError {
  case _fileCorrupted
  case _keyNotFound(_ key: String)
  static var fileCorrupted: Self { return ._fileCorrupted }
  static func keyNotFound(_ key: String) -> Self { 
    return ._keyNotFound(key) 
  }
}

-1

1 Like

I think this is a premise some people disagree with. Including me. I think that the current behaviour is inconsistent and confusing, and that this addition closes that hole, and makes the language more consistent and simple.

The argument goes like this: Enum case constructors are indistinguishable from static func/var in every way except one — The enum cannot be used to fulfil protocol requirements.

enum Foo {
    case bar
    case qux(Baz)
}

struct Foo {
    static var bar: Self { /* ... */ }
    static func qux(_: Baz) -> Self { /* ... */ }
}

At call site, these behave identically. The syntax is the same, and the semantics are the same. So why can only the latter be used as a protocol conformance? This seems inconsistent to me. The first time I realized this, I was baffled and confused.

As for motivating use cases, for me I have a protocol like this:

protocol Defaultable {
    static var default: Self { get }
}

I use this in generic code where I want to default to a trivial value, in case some predicate fails, a value is nil or whatever. I use it in a property wrapper that allows missing values in my Decodable types.

I can trivially make String, Int, Array etc conform by creating an extension that returns "", 0, [], etc. However, I also have a few enums that already have a default case. It is impossible to make that enum conform, even though T.default would return exactly what I need in generic code.

5 Likes

I agree with @AnuzaMaxima that the presented use case are not very compelling. I even need to dig in to the discussion thread, and the pitch thread to figure out what they're used for. Maybe we can incorporate some into the proposal?

With that said, I'm quite neutral about the feature.

-100!no help to new features, only bring confusion and complexity to new users.

Can you explain this position a little more, so I can understand your point of view?

I simply do not understand this position. To me, the current situation is confusing and complex, and this proposal will remove complexity and make it simpler and more consistent, thus making it easier for new users. I have elaborated on my view point with an example a few comments up.

10 Likes

What is the special behavior you believe this proposal introduces?

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 - #58 by michelf 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 - #117 by hisekaldma - 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).