Enum cases as protocol witnesses

Thanks for providing a more concrete example. I’ll note that in this particular case, the same technique applied for Book could be applied for Genre. That is, instead of case mockInstance, one could write:

static var mockInstance: Genre { .scifi }

That doesn’t seem like much of a lift.

That said, I’m still ambivalent on the pitch. On the one hand, it eliminates an inconsistency in the type system (given a certain characterization of enums). On the other hand, it introduces additional inconsistency in the explanation of how to make a type conform to a protocol.

One way to approach the latter concern is to think about what an IDE should offer as fix-its when a user declares that an enum conforms to a protocol. Should the fix-its be static vars and funcs, or should they be cases with associated values?

I look forward to seeing this proposal evolve and hope it can be refined to address the somewhat visceral reactions it seems to evoke in the present form.

1 Like

I don't think the existing fixits should change. That would mean making the assumption that the type's representation matches the static requirements of the protocol. That's sometimes the case but it is not a safe assumption in the general case. Adding a case could be a secondary fixit though.

I think the design of the proposal is fine. It just needs more concrete motivation. Unfortunately, I don't have time to look back through old code and refresh my memory of the use cases I've run into right now.

Two orthogonal features generally should always be able to compose. If there's a case where the composition breaks (like using enum cases to pose for protocol static members), it's worth fixing even if there isn't currently a "necessary" case for it. In fact, there's a vicious circle here; there's no necessary case to justify fixing the bug because the bug prevents us from creating necessary cases! Yes, there are generally workarounds, but that shouldn't stop us from fixing problems.

5 Likes

Oh! In that case it’s perfect, just badly rationalized, and the pitch makes a lot of sense too.

3 Likes

Yeah, I think it makes sense to offer two fix-its to insert missing protocol stubs - one that inserts them as 'static' and one as 'case'.

What really bugs me in this thread is that it is full of claims that are unproven (or maybe even plain wrong).
You may not like my tone, but I always try to be constructive, answer all serious questions and support my claims with evidence - I really wish the enthusiastic supporters would simply stick to the same practice...
Both enums and protocols have a huge "fanbase" here, so I wouldn't be surprised if critical voices shun this discussion to avoid confrontation.

Sounds like a truth - but is it an argument for the pitch?
I can already compose protocols and enums easily. On the other hand: if it's 100% true, then it should also be possible to have, for example, enums with reference semantic, shouldn't it?

This is suggestive wording, and it's only one of many examples in this thread: When something is a bug, it should be fixed, right?
But you may as well consider the change to be a severe bug, because suddenly you completely break the character of a protocol as a blueprint for conforming types. I'm fine with people having strong preferences, but not with pretending that everyone else is wrong.

That is actually the thing that bothers me the most - so thank you for admitting the lack of examples.
The point is, supporters of this change have been claiming that they have lots of cases that would benefit from the change - so why is it impossible to present a single example that supports their assertion?

Maybe. In this case, there is a difference - enum cases don't already have reference semantics, but do already have the semantics of static properties and functions. You are free to create a proposal that adds reference semantics if you want.

I don't think this change does that, given cases already behave as static properties and functions.

I think plenty of people in this thread have provided an example (like this), it just seems to me that you're not convinced with them and I don't think that is their problem (asking people for more examples is fine, but attacking them isn't). Plenty of people have expressed that they have a need for this feature, which I don't think should be dismissed.

2 Likes

I think I agree with you that the may not be considered a bug, however:

Please follow your own words:

… and provide evidence!

When you declare an enum case with associated values, a static function on that type, which constructs the corresponding case value, is declared. This is true, however one feels about it. Whether by design, or a happy accident, I don't know. But it is true, and it this fact is exploited in real code. They even have an established name: case constructors.

enum MyEnum {
   case aCase(value: String)
}

let f = MyEnum.aCase
// f's type is (String) -> MyEnum

This can be used as a static function in every single use case, with identical syntax throughout the language, as a higher order function, in maps, through indirection, or called directly. Except for one glaring omission: It cannot be used to satisfy a protocol requirement.

Elsewhere in the language, we don't have to explicitly declare a protocol requirement on a type for it to be considered conforming. It is sufficient that the required functions are defined through a default implementation, synthesised or whatever, as long as the required function somehow is made available on the type.

But not so for case constructors.

Is it a bug? Maybe not, but it certainly is inconsistent. And filling this gap, certainly does not "suddenly […] break the character of a protocol as a blueprint for conforming types". It just makes the implicitly declared static case constructor able to fulfil the role of the protocol. Just as any other implicitly declared functions can.

9 Likes

I think this might be where some people have an issue - the semantics might be those of static properties/functions, but this isn't necessarily obvious or intuitive, and it might feel like the similarity is more coincidental or superficial and based on how things could/do work behind the scenes, rather than based in a genuine relationship.

There's also the issue that what passes for a conformance of part of a protocol isn't always going to be very clear, and I can imagine people using the feature for the first time becoming frustrated, or potentially being surprised about how exactly the witness maps their implementation. Perhaps there's some way that XCode or other IDEs can make the relationship clear?

That said I'd love to see this implemented. This closes a gap, makes the language more consistent and cleans up a lot of code, without adding any syntax to the language.

2 Likes

This is just another counterproductive distraction: I think it's very clear that don't want such a proposal, but rather made an example why the argument of "composition everywhere" is moot.

Then how about simply answering my question regarding documentation? Unless you replace case with static ..., there is clearly a break.

So one is plenty? Well, there is even a second (still not much) example, but neither of those is convincing: With a protocol, you loose "switchability", which isn't the case when you stick to the established pattern of nesting.
Please note that I'm actually helping your pitch by asking for examples!

Now it's getting really weird: Call out where I attacked someone (not something), or please stay away from formulations that imply that I did so (something that can be considered as a sneaky attack on its own).

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