Enum cases as protocol witnesses

Someone already replied to your question here - I think class and static are spellings and we can certainly update the documentation to mention case as well.

What I was saying was that you said people haven't provided a single example, where as they have when requested. I encourage you to reach out to those people if you don't find them convincing.

Perhaps it's not the right word and I am sorry for using it. I was referring to your past tone.

That wasn't even a claim that needs evidence, but as it's rather easy:
case != static ... - that's it.
Formulations like "it's a fact" or "it's undeniable that" (or simply using certain words as if they were undisputed) imho should be backed by evidence, but that is not what I did - I don't even think the change aims to introduce a bug, and merely pointed out that this is nothing but a personal preference.
Please let's all just accept other peoples standpoint instead of pretending that there is an universal truth (unless you can bring a proof).

Currently, I can take a bunch of lines from a type, enclose it in a protocol declaration - and I have a protocol.
With enum cases, this would be completely different.
Yes, it can already be slightly different with class - but existing special cases are a poor rationale for new exceptions.

Note that I don't think this is a dealbreaker; in fact, I consider it to be a quite weak argument.
However, without proper examples to justify this change, even weak arguments have their weight...

I want to reiterate that I'm giving this idea the best possible support: Remember, we are in the pitch phase here - shouting "but this is great!" does not improve a proposal; collecting arguments does.

2 Likes

class instead of static is not an exception. Semantically, and from the standpoint of the user, "static" means "callable from the type itself rather than from an instance of that type". This is what a user cares about, this is what should be taught. As an example from another language, the way you declare a static member on a type in Kotlin is by declaring a companion object with an instance member, for example (from the language reference):

class MyClass {
    companion object Factory {
        fun create(): MyClass = MyClass()
    }
}

val instance = MyClass.create()

No one cares about the implementation detail here: the requirement is the ability to call the .create() method on the type MyClass itself, rather that on an instance.

A Swift enum case is 100% a constructor of that type, that is, a "static" function/member that produces an instance of that type. Therefore, an enum case would respect the semantics of a protocol that declares constructors. This makes sense from a purely theoretical standpoint.

I turn the class argument on itself: because in the case of classes a static member requires class instead of static, then

is irrelevant. A protocol in Swift should not be taught as "an abstraction constructed by copypasting member declarations from a concrete type": this is not true even for other protocol features, for example properties are always written with var and the mutability must be specified by the { get set } declarations.

I can't think about a better argument than "by itself, an enum would 100% respect the semantics of a certain protocol" to support the pitch.

9 Likes

I think this is a great argument for this pitch: protocol requirements can already be satisfied with other keywords and features than those used to define the requirement in the protocol. A protocol requirement var property: String { get } can be satisfied by a let property: String = "abc" even though the keywords don’t match, and even though a variable and a constant are different things. What matters is that the semantics match.

That’s a great feature! Expanding that feature to case constructors would be great too, and would only make the language more consistent.

3 Likes

Yes, it's one of the main reasons behind the Protocol Witness Matching Manifesto, to decide what differences are reasonable between a protocol requirement and a witness.

1 Like

Since some people are asking for examples where this pitch would be useful, I’ll give a real world example from a current project. Enums are great when modeling grammars of different kinds. Let’s say we’re modeling CSS properties, and we need to represent <length>:

enum CSSLength {
    case px(Double)
    case em(Double)
    case vh(Double)
    case vw(Double)
}

But then we realize that some CSS properties take <length> | <percentage>, some take <length> | <percentage> | auto, and some take <length> | <percentage> | <number>. So we represent those too:

enum CSSLength {
    case px(Double)
    case em(Double)
    case vh(Double)
    case vw(Double)
}

enum CSSLengthPercentage {
    case length(CSSLength)
    case percentage(Double)
}

enum CSSLengthPercentageAuto {
    case length(CSSLength)
    case percentage(Double)
    case auto
}

enum CSSLengthPercentageNumber {
    case length(CSSLength)
    case percentage(Double)
    case number(Double)
}

When we switch from using CSSLength to one of the other types, we’d prefer not to change any of the call sites. Going through the entire test suite and changing .px(10) to .length(.px(10)) would be a chore and would hurt readability.

So we define a protocol for anything that can be expressed as a <length>:

enum CSSLength {
    case px(Double)
    case em(Double)
    case vh(Double)
    case vw(Double)
}

enum CSSLengthPercentage {
    case length(CSSLength)
    case percentage(Double)
}

enum CSSLengthPercentageAuto {
    case length(CSSLength)
    case percentage(Double)
    case auto
}

enum CSSLengthPercentageNumber {
    case length(CSSLength)
    case percentage(Double)
    case number(Double)
}

protocol CSSLengthExpressible {
    init(_: CSSLength)
}

extension CSSLengthExpressible {
    static func px(_ value: Double) -> Self {
        .init(.px(value))
    }
    static func em(_ value: Double) -> Self {
        .init(.em(value))
    }
    static func vh(_ value: Double) -> Self {
        .init(.vh(value))
    }
    static func vw(_ value: Double) -> Self {
        .init(.vw(value))
    }
}

extension CSSLengthPercentage: CSSLengthExpressible {
    init(_ length: CSSLength) {
        self = .length(length)
    }
}

extension CSSLengthPercentageAuto: CSSLengthExpressible {
    init(_ length: CSSLength) {
        self = .length(length)
    }
}

extension CSSLengthPercentageNumber: CSSLengthExpressible {
    init(_ length: CSSLength) {
        self = .length(length)
    }
}

That definitely works, but having to write the protocol conformance manually for each type is clearly just boilerplate. With this proposal, it would just become:

enum CSSLength {
    case px(Double)
    case em(Double)
    case vh(Double)
    case vw(Double)
}

enum CSSLengthPercentage: CSSLengthExpressible {
    case length(CSSLength)
    case percentage(Double)
}

enum CSSLengthPercentageAuto: CSSLengthExpressible {
    case length(CSSLength)
    case percentage(Double)
    case auto
}

enum CSSLengthPercentageNumber: CSSLengthExpressible {
    case length(CSSLength)
    case percentage(Double)
    case number(Double)
}

protocol CSSLengthExpressible {
    static func length(_: CSSLength) -> Self
}

extension CSSLengthExpressible {
    static func px(_ value: Double) -> Self {
        .length(.px(value))
    }
    static func em(_ value: Double) -> Self {
        .length(.em(value))
    }
    static func vh(_ value: Double) -> Self {
        .length(.vh(value))
    }
    static func vw(_ value: Double) -> Self {
        .length(.vw(value))
    }
}

Fewer lines, and a much clearer expression of the programmer’s intent. And that’s just for a small part of CSS. If we were to model the entire CSS grammar, this proposal could make a huge difference, both in terms of lines saved and in terms of clarity.

In fact, I think this proposal will be broadly useful whenever you need to migrate from structs to enums, from enums to structs, or between different levels of enum nesting. The ability to use cases and static functions interchangeably makes that type of migration not just manageable, but even relatively easy. This proposal would expand the scenarios where cases and static functions can be used interchangeably, making that type of migration even easier.

16 Likes

To add some weight to the CSS example, I have a CSS generation library where I'm solving the exact CSSLengthExpressible problem with code generation. Would be great if I could use protocol extensions for this.

@hisekaldma Maybe you should split your last code block into two parts, took me some time to figure out that it's scrollable.

1 Like

I like this example a lot, and it makes a good case for me to support the proposal.

3 Likes

I agree! And I have. It is an observable fact that a static case structor function is made available on type from a case declaration with associated values. I have provided examples and clearly stated the premises for the statement.

Sure, sometimes you can. But not always. However, if we follow your example and look at the transformation from conformances to protocol, we still don't have a one-to-one syntactic correspondence:

  • You can't take a let from a type and copy it into a protocol declaration. You must turn it into var { get }.
  • You can't take class func, you must turn it into static func
  • You must sometimes add mutating (or nonmutating) to your declarations for them to work in protocols, even if they weren't needed in the type you copied from.

You can also look at it from the other direction, and look at the transformation from protocols to conformances. We have ample precedent in Swift that protocol conformances do not enumerate all the lines from the protocol definition verbatim:

  • A class func can fulfil a static func requirement
  • A let can fulfill a var { get } requirement
  • A default implementation in the protocol itself can fulfil any protocol requirement on a type.
  • A compile time synthesized conformance for a type can fulfil a protocol requirement
  • A func signature can fulfil an associatedtype requirement

In general:

Whenever a type somehow gets the requirements, the type is considered conforming to the protocol. Copy-pasting declarations and adding method bodies are one way of adding conformance, but not the sole way.

  • It is fact (yes, fact!) that enums with cases with associated values gets a static case constructor.
  • It is fact (again!) that such case constructors does not currently fulfil a protocol requirement, even if the protocol requires a function with the exact same signature and scope as the case constructor.
  • It is my opinion (yes, opinion) that this is strange and surprising.
9 Likes

Sounds like the use case for all these shortcuts is to create a nicer CSS-like language in Swift, something like this:

.rule("div.someClass", [
    .maxWidth: .em(112), // shortcut for .length(.em(112))
    .minWidth: .px(10),  // shortcut for .length(.px(10))
])

I've done some similar things with enums in the past, including creating shortcuts similar to that. I never had enough of them that I felt the need to abstract things in a protocol though.

I'm not sure this and the JSONDecodingError example in the proposal would be a good enough use cases to push a new language feature, even though it does seem useful. But we aren't really talking about a new feature here, right?

Enum case constructors are already treated as static vars or static functions pretty much everywhere in the language already. They don't for the purpose of conforming to a protocol. I think in increases the coherency of the language to allow a conformance to work in this case. I don't think the threshold for convincing use cases should be too high here.


I must admit that case sometime matching var and at other times matching a func is a bit repulsive. It looks incoherent... but I think we must realize that this incoherency is already there. Extending @sveinhal's previous example:

enum MyEnum {
   case aCase(value: String)
   case basicCase

   static func aFunc(value: String) -> MyEnum { … }
   static var basicProp: MyEnum { … }
}

let a = MyEnum.aCase
// a's type is (String) -> MyEnum
let b = MyEnum.basicCase
// b's type is MyEnum

let f = MyEnum.aFunc
// f's type is (String) -> MyEnum
let p = MyEnum.basicProp
// p's type is MyEnum

Perhaps it's worth fixing, in which case it might be wise to reject this proposal before it locks us in a bit more. But if we don't intend to change this in the future, then I think implementing this proposal is the right thing to do.

2 Likes

“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