Enum cases as protocol witnesses

The compiler doesn't enforce O(1) semantics on subscript because it can't; but this is not a prescription that it shouldn't where it is possible. In other words, we do not have to enable semantically nonsensical conformances simply because we can make the syntax fit, if there are alternative options to enable the semantically sensible use cases without doing so.

Can an enum have two cases seconds(Int) and seconds(Double) now? I was under the impression that this was not supported: SE-0155 still requires cases to have "distinct full names."

It's not possible now, but I believe it would be reasonable for overloading to work. I don't know if it should work without any labels though.

Anyway, in this case, you will have to provide a manual implementation for seconds(Double) but the remaining requirements could be satisfied by the cases directly.

So, hypothetically, Apple could’ve done this instead:

extension DispatchTimeInterval: SchedulerTimeIntervalConvertible {
  public static func seconds(_ s: Double) -> Self { 
    return DispatchTimeInterval.seconds(Int((s * 1000000000.0).rounded())) 
  }
  // Remaining requirements already satisfied by cases
}

Of course if in the future we allow overloading and DispatchTimeInterval gains a seconds(Double) then the above would simply become extension DispatchTimeInterval: SchedulerTimeIntervalConvertible {}.

Similarly, the compiler can’t enforce that enums only witness these “factory” static func requirements—it can only add syntactic hoops that someone has to jump through in order to witness the non-factory requirements (either with underscored cases or some alternative method to this proposal). We could introduce annotations like @constantTime to force protocol conformers to syntactically document their time complexity (even if conformers went on to violate that contract), but it’s been deemed sufficient so far to allow protocols to declare most of their semantics as documentation. I’m not convinced that this issue rises to the level of importance (as opposed to say, mutating vs. nonmutating) to require syntactic barriers to conformance.

1 Like

Right, SE-0155 allows foo(x:) and foo(y:) to be distinct cases; the implementation work is not done.

However, allowing foo(_:) and foo(_:) to be overloaded with only differences in type is explicitly disallowed by SE-0155 and would not be supported without further proposals, unless there's an enum evolution proposal I've missed along the way somewhere.

The more interesting question to consider, in my view, is whether we may ever implement generic cases, and with that whether a generic case would be able to count as the implementation for both seconds static function requirements.

For the utility of annotating factory semantics, see prior discussions such as the following: [Proposal] Factory Initializers.

1 Like

The proposal says:

A drafted version of this proposal considered allowing "overloaded" declaration of enum cases (same full-name, but with associated values with different types). We ultimately decided that this feature is out of the scope of this proposal.

I am not sure if such a change would need to go through evolution again though.

Most definitely so, I'd imagine. It was explicitly scoped out of the prior proposal (as in, not accepted as part of it).

I suppose. You can sort-of get around it with static functions though. For example, this is valid today:

enum Foo {
  case seconds(Int)
  static func seconds(_: Double) -> Self {
    return Foo.seconds(...)
  }
}

or

enum Foo {
  case seconds(Int)
  static func seconds(_ s: Double) -> Self {
    return Foo.seconds(Int(s))
  }
}

or even this:

enum Foo {
  case seconds(s: Int)
  static func seconds(s: Double) -> Self {
    return Foo.seconds(s: Int(s))
  }
}

Thanks for the thread! A couple of thoughts:

I'm not sure that that thread makes the case for annotating factory semantics—it seems like what was settled on there was to allow return/self assignment within convenience initializers, which actually hides the factory semantics below the API. But even if we allow for factory semantic annotations on concrete types, that still doesn't necessarily imply that it should be relevant at the protocol level. We don't allow class-bound protocol to specify convenience initializers today.

Also, if a protocol author has created what is essentially a factory static func requirement on the protocol, but for whatever reason has not marked it as such, why should the burden then be placed on conforming enum types to jump through hoops in order to conform properly?

I'm still not convinced that the FloatingPoint examples from above are really such a bad fit. I think the objection arises from the fact that those static func requirements feel more like transformations than strictly factories, at least in part because they take Self-typed params as input. Are there examples of static func requirements that take no Self-typed arguments but still don't seem like a good fit? Drawing the line along those boundaries would boil down a rule something like "static func requirements may not be satisfied by an indirect case".

1 Like

Good!

Swift has generally avoided requiring boilerplate-y annotations like this. We often put semantic requirements in documentation and expect conformers to be responsible in meeting those requirements. I don't see why requiring a factory annotation would make the language better here.

In the numerics domain, I think it is extremely unlikely that anyone would even consider writing an enum with cases that fulfill static requirements. I don't think we need the absence of an annotation to guide people away from that.

This is largely irrelevant to the present discussion which focuses on cases where we want factory method requirements. The thread you reference discusses supporting factory initialilzers - i.e. those which are allowed to return a subtype of Self - as a preferred alternative to factory methods in some cases.

This is in many respects the opposite problem. In the example I gave, I explained how I decided to use initializer requirements where I would have preferred a factory method requirement (so users would not have to compromise their case names). In the thread you link people want to use an initializer where they currently have to use a factory method.

3 Likes

Hold up. This is (a) conflating language semantics (*) with the semantics of the functions being expressed in the language; and (b) flipping the argument presented in the pitch on its head:

I think the FloatingPoint example is at least one major instance where we can see that the similarities in language semantics fall short. It bears examining whether the argument from the point of similarity motivating this pitch is sufficiently strong to extend the slam dunk case of conformance to static property requirements to static function requirements.

(*) I am guilty of the above as well at times. To me, the term means: Two language features x and y have similar semantics to the extent that they can be used in similar ways by users of the language to accomplish a particular task (e.g., computed properties and stored properties, class methods and static methods). Semantics here would be in contradistinction to how the underlying implementation of the features may be similar or differ at the level of the compiler or runtime, or how superficially the spelling of x may be similar or differ from y. This is altogether distinct from the semantics of any particular thing that I create using language feature x or y (for instance, a specific computed property foo or bar). It is true that the language does not usually enforce the semantics of foo or bar; but by construction, the language does enforce language semantics.

We are going far afoot from the issue of enum conformance to protocols, but we are not venturing into an alien planet in terms of language semantics here. The question is very much live in the sense that we are discussing: How do we want to spell factory patterns in Swift? And in what way should features with distinct spellings but share these language semantics (i.e., that of factories) interact in terms of conformances, subclassing, etc.?

Now, perhaps you would argue that factory patterns should not have their own spellings--which gets us to the same endpoint in terms of what you're arguing concretely here: one shouldn't need to annotate static function requirements as factories and the language shouldn't care.

But I find that argument unconvincing for two reasons: (1) in the case of enum cases, we have just agreed that cases with associated values do have the language semantics of factories, so we already have one way of spelling this in the language; (2) as revealed in the thread I linked to, there is demand for a second way of spelling factories in the language and in fact the underlying implementation already exists to support Obj-C and just doesn't yet have a Swift-native syntax.

I don’t understand this objection. No one is proposing that all static functions that return self are semantically identical to cases with associated values. No one is proposing to “bless” anything.

This proposal is centered around the fact that (1) enums currently already exposes static constructors for its cases, and that these follow the same language semantics as static functions, including leading dot syntax, referencing the bare constructor without invoking it, etc. and that (2) sometimes it is useful to make these constructor functions (or simple cases) take the role of protocol requirements.

6 Likes

Example of use case: https://stackoverflow.com/q/54769932/1311272

2 Likes

My case was similar. I was experimenting with a zero-bit integer type. That is not allowed for signed integers, but is OK for unsigned integers. I first made an empty struct, then I realized that I could make the single state an enum instead. It should be seamless if I called that case .zero, but the issue of this thread prevents this (for now).

1 Like

Is this still actively being discussed? This is something I've wanted to do (and was surprised to find was not already supported) a few times over the past few months, and this thread is the only discussion I can find about it.

It was reviewed and implemented as SE-280, and should be available in recent compilers.

3 Likes

Thank you!

That reminds me on my first thought about the pitch — and it looks like that has been ignored completely:
There is still no official documentation about this feature at all, isn't it? At least I couldn't find it in Index of /swift-book/LanguageGuide.
Imo this situation is quite unsatisfactory — how are people supposed to know what code means which depends on undocumented behavior?

There is documentation available for it. If you look at the “Protocol Declaration” section here: Declarations — The Swift Programming Language (Swift 5.6) and scroll down a bit you can see it.

2 Likes