Enum cases as protocol witnesses

“Fixing” this would require a significant breaking change. If we did that, I think the obviously design is to require parentheses both in the case declaration and at the usage site:

enum Foo {
    case bar()
}
let fooBar = Foo.bar()

I think a change like this is extremely unlikely to be accepted. The source breakage is significant. Further, I suspect that many people would argue that it introduces syntactic noise they don’t want to be forced to write.

I think it’s safe to move forward with this proposal without needing to be concerned about a change like this happening in the future.

2 Likes

Yeah, it would definitely be very source-breaking to re-introduce the case foo() syntax again now after SE-0155 banned it. It will also require a change to the grammar. Even when case foo() was allowed, it actually meant case foo(Void). Today, writing MyEnum.foo() is only allowed when all arguments are default.

The proposal, as it stands, is purely additive and does not cause any source breakage.

1 Like

I Appreciate the apology! A lot of this stuff can become hand-wavey pretty quickly, so I think a lot of it comes down to trying to see the other side of things and giving people the benefit of the doubt. Which we can all improve on.

To address the general issue, as a concept gets more complex I find it's increasingly hard to come up with meaningful, concise examples. Especially when starting from the feature rather than a concrete use case. Although this has come up for me, it's likely < 10 times with none I can recall in detail.

There's also the complication of being a static-level feature where use cases tend towards meta-programming which has it's own additional complexities.

I think this results in it seeming like an obvious win for those who have come across an instance where they want the feature, but it being difficult to explain find a concise use case.

To give a couple more specific examples...

*note* as I don't have a build with the feature there may be minor mistakes due to not being able to compile

First, a Generic Encoder Type that provides a consistent API between different encodings. (I would still want to play with the structure more before it would be something in production). A Decoder and Combined version could be easily imagined, but the basic pattern lends itself to some Constructor patterns and really anything where you want to do the same action with multiple things.

protocol EncoderProvider {
    static func json(_ dateEncodingStrategy: JSONEncoder.DateEncodingStrategy): Self
    static var plist: Self { get }
    static var xml: Self { get }
    static var yaml: Self { get }

    func encode<T: Encodable>(_ item: T) throws -> Data
}

enum BasicEncoderProvider: EncoderProvider {
    case json(JSONEncoder.DateEncodingStrategy)
    case plist
    case xml
    case yaml

    func encode<T: Encodable>(_ item: T) throws -> Data {
        switch self {
        case .json(let dateEncoding):
            let encoder = JSONEncoder()
            encoder.dateEncodingStrategy = dateEncoding
            return try encoder.encode(item)
        case .plist:
            return try PropertyListEncoder().encode(item)
        case .xml:
            return try SwiftyXMLEncoder().encode(item)
        case .yaml:
            return try YAMLEncoder().encode(item)
        }
    }
}

struct GenericEncoder<Provider: EncoderProvider> {

    // Generic
    static func encode<T: Encodable>(_ item: T, with parser: Provider, completion: @escaping (Result<Data, Error>) -> ()) {
        DispatchQueue.global().async {
            func finishedAction(_ result: Result<Data, Error>) {
                DispatchQueue.main.async { completion(result) }
            }
            do {
                let data = try parser.encode(item)
                finishedAction(.success(data))
            } catch {
                finishedAction(.failure(error))
            }
        }
    }

    //Specific
    static encodeJSON<T: Encodable>(_ item: T, completion: @escaping (Result<Data, Error>) -> ()) {
        encode(item, with: .json(.secondsSince1970), completion: completion)
    }

    static encodePList<T: Encodable>(_ item: T, completion: @escaping (Result<Data, Error>) -> ()) {
        encode(item, with: .plist, completion: completion)
    }
    // Etc.
}

One could argue (probably even me) that this may be better expressed in a class/struct, but part of the point of a protocol is to leave it up to the user how a construct is best expressed in their codebase. That's where this proposal improves the ergonomics, readability, and maintainability when adhering to a protocol with an enum.

Here's another idea that allows setup of a Button's different states:

protocol ButtonState: Equatable {
    static var disabled: Self { get }
    static var normal: Self { get }
    static var selected: Self { get }

    var title: String { get }
    var titleColor: UIColor { get }
}

extension ButtonState {
    var controlState: UIControl.State {
        switch self {
        case .normal: return .normal
        case .selected: return .selected
        case .disabled: return .disabled
        default: return .disabled
        }
    }
}

class ModelBackedButton<State: ButtonState>: UIButton {

    required init?(coder: NSCoder) {
        super.init(coder: coder)
        doSetup()
    }

    private func doSetup() {
        Array<State>(arrayLiteral: .normal, .selected, .disabled).forEach(updateValues)
        updateViews()
    }

    private func updateValues(for buttonState: State) {
        setTitle(buttonState.title, for: buttonState.controlState)
        setTitleColor(buttonState.titleColor, for: buttonState.controlState)
    }
}

enum OKButtonState: String, ButtonState {
    case disabled = "--"
    case normal = "OK"
    case selected = "ok"

    var title: String { rawValue }
    var titleColor: UIColor {
        switch self {
        case .disabled: return .gray
        case .normal: return .black
        case .selected: return .darkGray
        }
    }
}

enum LocalizedOKButtonState: String, ButtonState {
    case disabled = "--"
    case normal = "OK"
    case selected = "ok"

    var title: String { NSLocalizedString(rawValue, comment: "...") }
    var titleColor: UIColor {
        switch self {
        case .disabled: return .gray
        case .normal: return .black
        case .selected: return .darkGray
        }
    }
}

// Using a subclass here rather than a typealias to avoid any issues when used in InterfaceBuilder
class OKButton: ModelBackedButton<OKButtonState> { }
class LocalizedOKButton: ModelBackedButton<LocalizedOKButtonState> { }

Again, this would need to be more fleshed out for a production, but the basic concept could be used for a variety of UI controls. (radio buttons, Switches, etc.)

3 Likes

I'm not disagreeing with this; it seems quite unrealistic to "fix" this, assuming this needs fixing at all. It was just my conclusion that we should either acknowledge the inconsistency that sometime a case behaves as a static var and at other times it behaves as a static func by formalizing it into protocol syntax, or we should redesign case to not have this inconsistency. Keeping the status quo is somewhat akin to denying reality, but that's an option too!

1 Like

I’ve seen a few posts calling out the fact that the case keyword exists and is distinct from static for a reason in arguments against this proposal making sense (I’ll refrain from quoting or linking, not to avoid confrontation, but rather to reply in general).

I propose that we think of case not as a keyword intended to distinguish cases from static members but rather to distinguish canonical static constructors of enumerated types from “other” (I.e “convenience”) static constructors of those same types. The distinction is there to be explicit about what values are allowed, not to create an entirely separate non-static concept. That might be more opinion than fact, but it’s how I think about cases.

2 Likes

This is how I've always thought of them as well. Especially before cases with associated values were allowed to have default values, I would commonly write code like this to simplify the call sites of enums that I was using to model different complex states:

enum Foo {
  case bar(Int, someCondition: Bool)

  // A convenience with both values defaulted
  static let bar = Foo.bar(0, someCondition: false)

  // A convenience with the boolean defaulted
  static func bar(_ value: Int) -> Foo { return Foo.bar(value, someCondition: false) }
}

(Of course, this is no longer necessary with default values—with one subtle difference being that instead of getting a static let for the everything-defaulted case, I would use a no-argument function call.)

In other words, the case constructors are like static let/funcs that also fully define the "shapes" of the permitted values of that type, which is why those are used to determine exhaustiveness and pattern matching, but the similarities to other static declarations is what lets us alias them in this fashion to create convenience constructors.

6 Likes

I have read this proposal and decided to join a resintance side. Not because it seems to me as unnecesary addition that only brings unnecessary syntactic exception as @Zhu_Shengqi and @Tino had mentioned, but also one example cant get out of my mind .

protocol P {
  static func f() -> Self
}
enum E: P {
  case f
  case t
}
var some = E.f //conforms to P
some = E.t     //now it doesnt? :flushed:
func willCrushInRuntime?(_ arg: P)
willCrushInRuntime?(some)

I believe it is time to summon our forefather.
cc @Chris_Lattner3, they want to make your great language worse. :worried:

It is explicitly stated in the proposal draft that a no-argument static function will not match with an enum case with no associated values. So, that won’t compile.

Oh. My bad. Cherry picked it from this guy @allevato . :slightly_smiling_face:

btw, it sound even more unconvincing. -2 to this proposal.

This pitch is an absolute no-brainer to me. I'm surprised at the early negativity. This is an extremely natural and fitting extension of passing case constructors (for cases with associated values) as functions, which we already have.

Enum case constructors are not meaningfully distinct from static var, static func, or class declarations on the type. When you run into the issue that this pitch resolves, it's clear that the language already thinks this way:

protocol Foo {
    static var bar: Self { get }
}

enum Baz: Foo {
    case bar

    static var bar: Self {
//             ^
// Error: invalid redeclaration of 'bar'
        return .bar
    }
}

How can it be considered a redeclaration if these two declarations don't declare semantically similar things already?

As to those looking for more concrete examples, I point to Combine's SchedulerTimeIntervalConvertible. It's clearly meant to be something enum-ish, but instead of being able to retroactively conform DispatchTimeInterval from GCD, a different, much more complicated struct type (DispatchQueue.SchedulerTimeType.Stride) is needed to perform the same job.

There are several ways for a protocol to imply a request for a specific type of implementation; flagging or not flagging a method with mutating, a : AnyObject constraint, etc. Requesting something enum-like is clearly possible and clearly supported, and it seems silly that such a protocol cannot be conformed to by an actual enum.

9 Likes

This is in fact almost exactly backwards: the similarity is in language semantics, and at the implementation level there is really very little similar between an enum case construction and a static method call.

This pitch is centered on a claim that protocol conformance should be based on this kind of semantic similarity rather than more superficial considerations.

13 Likes

I'm in favor. It's always been annoying that you can't use enums to conform to protocols as easily as structs or classes.

6 Likes

I’ve been aching for this feature for what feels like ages! My main use case is an event-driven UI (and not only UI) architecture:


public protocol InputEventHandler {
    associatedtype InputEvent
    func handle(_ event: InputEvent)
}

public protocol CanCloseInputEvent {
    static var close: Self { get }
}

public protocol CanMinimizeInputEvent {
    static var minimize: Self { get }
}

public func doSomethingWith<W>(window: W) where W: InputEventHandler, W.Event: CanCloseInputEvent & CanMinimizeInputEvent {
    // ...
}

struct MyWindow: InputEventHandler {
    enum InputEvent: CanCloseInputEvent, CanMinimizeInputEvent {
        case close
        case minimize
    }
    func handle(_ event: InputEvent) {
        // ...
    }
}

3 Likes

Why? Can you elaborate on this? Many people in this thread have shown lots of examples and presented extremely solid arguments in favor of something that's basically a syntactic hole in a semantic wall that's perfectly sound. This "no-argument static function problem" is very, very minor, barely relevant, and doesn't undermine at all the general utility of this pitch.

Also this seems like an overreaction. You posted an example that neither compiles (even with this proposal) nor makes logically any sense, at least from what the comments in the snippet would imply. You can‘t have a type to conform to a protocol and in an other instance of it, it wouldn‘t. This is your logical error there.

I also think it‘s not fair to call out to Chris and shouting out load that someone as talented and patient with our attitude as @suyashsrijan would try to make the language worse. His contribution to the language grows and grows. We should appreciate his time and energy and remain civil in the discussion or we might lose another great contributor.

8 Likes

My main concern is still that with this approach for language evolution which is 'lets add a bunch of discrepant cases in every place that feels wicked ' we might end up with something c++esque, where the variety of approaches has increased so much it is pain to develop in this language. So I wouldnt appriciate programming with swift had become a sport for devoted ones. Also one of main swifts's worths is that it has a shallow learning curve(at lest that how it was for me) compared to other programming languages( with python set aside:) ).

You mentioned presence of many good examples, but with first example from this thread being picked, I don't see how it would be an easy task for me to deal with that kind of code. Same can be said about the rest.
[reference to example: Enum cases as protocol witnesses - #5 by davdroman]

I definetly didnt intend to get anyone retired from contributing to swift. And @suyashsrijan surely deserves a credit: working with compilers is not a trivial task. But this was not addressed to his persona neither it meant to be a sneer.
My point is still that i dont see how this can benefit community at large.

That is even more a disclosure :flushed:. The example you are referring to was mostly for demonstration. Sorry for that :face_with_hand_over_mouth:. But I still cant get it: in cases where there would be a protocol with one requirement and enum with two cases one of which makes it conforming to the former protocol, would it be possible to reassign it with new value so that enum doesn't conform anymore? :thinking:

Afaics: The assignment is possible, but not a problem (for the compiler).
That is because it's the enum as a whole which conforms, and not a specific case:
The requirement is marked static, so it isn't tied to an instance.

Still, your post emphasis an argument that has been brushed away frivolously before:
This feature can confuse people.
The severity of this danger in relation to the usefulness of the change has to be judged by Core, and I hope they aren't biased by exaggeration from either side.

1 Like

No, this has nothing to do with the proposal nor is this regular Swift then.

You can have a struct with two static properties one of which satisfies the protocol requirement. If you use the second property and it happens to return Self, it does not mean that the instance no longer does not conform to the protocol.

As @Tino points out, the whole type conforms to the protocol not a single enum case. That is not what has been proposed. ;)

3 Likes

This use case has a lot of similarity with some of the use cases I've seen. Thanks for sharing it!