Enum cases as protocol witnesses

A case upc(Int, Int, Int, Int) would be matched by the requirement static func upc(_: Int, _: Int, _: Int, _: Int) -> Self, which is semantically equivalent. No parameter labels in the case declaration would match no parameter labels in a protocol requirement.

To be clear, the following code is valid and compiles:

protocol Example1 {
  static func upc(_: Int, _: Int, _: Int, _: Int) -> Self
  static func qrCode(_: String) -> Self
}

protocol Example2 {
  static func upc(_ first: Int, _: Int, _ third: Int, _: Int) -> Self
  static func qrCode(_ code: String) -> Self
}

enum Example {
  case _upc(Int, Int, Int, Int)
  case _qrCode(String)
}

extension Example: Example1 {
  static func upc(_ a: Int, _ b: Int, _ c: Int, _ d: Int) -> Example {
    ._upc(a, b, c, d)
  }

  static func qrCode(_ code: String) -> Self {
    ._qrCode(code)
  }
}

extension Example: Example2 {}

In the Example2 the labels first, third and code are meaningless, and don't contribute anything to the protocol requirement: I would mark them as errors in a linter, or comment negatively in a code review. Thus, semantically, they correspond to cases with associated values you mentioned.

I understand the pitch. We can create a static function that’s invoked with the same syntax as a case like the example above.

I’m arguing that I’m not sure static function requirements are in practice written with such semantics, at least not often. Even the naming conventions surrounding associated value labels and function argument labels differ. Do you know of any static functions in the standard library that read like this case?

I have in my current project a property wrapper called @DefaultDecodable that I use on Decodable structs to allow missing keys to be decided using non-optional values for wrapped types that have a reasonable ā€œtrivialā€ default value.

It is used to treat missing arrays as empty arrays, missing bools as false, etc. it is used throughout my current project because the backend for some reason remove ā€œemptyā€ values from the returned JSON responses.

The project in question is a video streaming app which presents content in so-called ā€œswimlanesā€ which are horizontally scrollable lists of films and episodes that the user can watch.

It is implemented like this:

struct Swimlane: Decodable {
    var id: String
    @DefaultDecodable var assets: [VideoAsset]
    // many more properties
}

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

extension Array: Defaultable {
    static var `default`: Array { return [] }
}

Recently, the swimlane type received a new field, style which best is implemented as an enum:

enum SwimlaneStyle: String, Decodable {
    case hero
    case featured
    case `default`
}

With this proposal implemented, I could simply make SwimlaneStyle conform to Defaultable, and mark the new field in my swimlane type as @DefaultDecodable and be done with it.

Now, since I cannot do that, I have to either write my own implementation of init(from:) which would mean writing hundred lines of boilerplate just to accommodate that one special property. Or write some single-use property wrapper. Or rename the default case in my enum to something like _default (adding swiftlint disable comment), explicitly adding my own CodingKeys enum just to map that one mismatched case name, and reexport the case as an explicit static var.

I went with the latter, to keep the complexity contained within that one type. Now consider the difference:

enum SwimlaneStyle: String {
    case hero
    case featured
    case _default //swiftlint:disable:this identifier_name
}

extension SwimlaneStyle: Decodable {
    case CodingKeys: String, CodingKey {
        case hero, featured, _default = "default"
    }
}

extension SwimlaneStyle: Defaultable {
    static var `default`: SwimlaneStyle { ._default }
}

versus this


enum SwimlaneStyle: String, Decodable, Defaultable {
    case hero
    case featured
    case `default`
}

5 Likes

Most static function requirements do not return Self so of course when measured against all static requirements, it will not seem like these kind of requirements are written often. The appropriate question is how many static factory method requirements (i.e. returning Self) would have semantics appropriately met by an enum case. With this narrower question, I suspect the answer is most of them. Certainly all the ones I have written or encountered.

4 Likes

To be fair, that example can also be written as:

case upc(_: Int, _: Int, _: Int, _: Int)

which would be equivalent and closer to the static func requirement syntax. It's just that nobody writes it that way because the unlabeled argument placeholders are noise.

Heck, you can even give the case actual argument names, and they don't do anything (that I'm aware of), but it still compiles (at least in 5.1):

case upc(_ useless1: Int, _ useless2: Int, _ useless3: Int, _ useless4: Int)

Essentially, the disconnect here is that enum cases allow for more shorthand by letting argument labels be elided completely and by not requiring actual argument names because there's no corresponding function body that needs to refer to them. But I don't think that matters here ultimately, because we're talking about protocol conformance, and so this relates back to the point that members of the Core Team have made in the past that the significance of protocols is that they serve as semantic requirements and not just bags of syntax.

I'd consider 2 things:

  1. naming conventions usually differ between static functions and enum cases, but that could (and probably should) change, especially after SE-0155 was accepted: the way I read that proposal is "let's use function representation rules for enum cases, because they're essentially the same thing"; I already do this, for instance using an argument label for enum cases with a single associated type;
  2. we're mostly talking about static functions that return Self, that is (let's say) constructors; these types of static functions have a specific place in Swift heart, because they allow for the "dot syntax", something that Swift definitely encourages and, to me, one of the main markers of "Swifty" code (other than being one of my favorite features of the language).

So I'd say that static function requirements are written with such semantics when they represent constructors, i.e., return Self.

I'd consider internal argument labels in case declarations and protocol declarations akin to "language detritus", a useless leftover that makes no sense, but it's still accepted by the compiler :smiley:

I've built a toolchain for testing purposes, in case anyone's interested:

macOS: https://ci.swift.org/job/swift-PR-toolchain-osx/477/artifact/branch-master/swift-PR-28916-477-osx.tar.gz
Linux: https://ci.swift.org/job/swift-PR-toolchain-Linux/346/artifact/branch-master/swift-PR-28916-346-ubuntu16.04.tar.gz

You’ll have to use it in Xcode as playgrounds don’t support newer toolchains right now.

1 Like

@suyashsrijan I removed Pitch: from the title yet again, as this is redundant information in a pitch subcategory.

Thanks, I added it out of habit but I agree it’s redundant!

1 Like

That is a good point. Can you share some existing examples of static method requirements returning Self which have similar semantics, to which protocols one would reasonably conform enums that would spell those requirements as cases?

As counterexamples, consider the requirements we have on FloatingPoint which would fall under the realm of this discussion. Syntactically, they fit the bill, but semantically, all of them would read rather absurdly as enum cases:

indirect enum HypotheticalFloat: FloatingPoint {
  /* ... */
  case maximum(Self, Self)
  case minimum(Self, Self)
  case maximumMagnitude(Self, Self)
  case minimumMagnitude(Self, Self)
  case `*`(Self, Self) // not yet possible, but see other recent pitch
  case `+`(Self, Self)
  case `-`(Self, Self)
  case `/`(Self, Self)
}

It seems absurd that numerical operations naturally expressed as static functions (of which we have more and more in Swift Numerics, such as the trigonometric functions) would ever be implemented as cases of a floating-point type.

I don't have a ready-made way of expressing this in words (yet), but I think all of us would agree that intuitively case sin(Self) feels wrong.


Let us use the same protocol to show how static properties as they are currently used are a perfect fit for cases without associated types, both syntactically and semantically:

enum HypotheticalFloat: FloatingPoint {
  /* ... */
  case nan
  case infinity
  /* etc. */
}

Does it seem plausible that, if we were to choose to write a custom floating-point type using enum, we might choose to represent NaN and infinity as their own cases? Definitely! This fits both semantically and syntactically.

Would we want to use case or a static computed property for greatestFiniteMagnitude? That comes down to internal details of how we want to represent values, almost exactly parallel to an author's decisions about whether we want to use a stored or computed property in a struct.


I guess what I'm arguing here is: I don't think there's any way we can say that static requirements returning Self either all or even mostly have the semantics of "factories". Certainly the examples I give above do not.

If we had a way to label factory functions in the language (and this would potentially be helpful in other ways--such as allowing for the expression of a more conventional class cluster factory pattern in Swift--see the multiple prior threads on Swift Evolution about this idea), then those static requirements specifically would be a perfect fit for cases with associated values.

But I am uncertain as to whether blessing "static requirements that return Self" as the semantic counterpart to cases with associated values is such a perfect fit.


I hope my reply to @anandabits better demonstrates that my concern is about semantics; I am not troubled one whit about whether the spelling of an implementation differs from the requirement in terms of the number of underscores and colons.

3 Likes

Actually, the code written there seems entirely reasonable to me as a type in some sort of calculator tool which aims to represent floating point types in terms of how they were built up from other floating point types, in order to, say, break a solution to a problem down into steps a la WolframAlpha. You could imagine further cases like .literal(Double) and a function like func compute() -> Double to actually extract the value from any given instance of this HypotheticalFloat type.

I haven’t thought through whether this model would really work with the rest of the FloatingPoint protocol, but I don’t think that the code as written is as absurd as you claim.

5 Likes

I don't think we disagree here. I don't doubt that such a type could exist (I guess in some parts of the previous post my wording could give off such an impression). What I doubt is that the conformance here is meaningful (the topic of this thread). That is, that it would actually be helpful to write useful generic algorithms which don't care whether you're operating on values of Double type or of this hypothetical calculator tool type. By the sounds of it, you have such doubts too.

I think we agree overall that such a hypothetical type would likely be very different from the FloatingPoint types one typically envisions. And this goes back again to this issue about semantics versus syntax: again, we can make all of this fit in terms of spelling, but I'm not certain that the semantics of static function requirements and of cases with associated values are always or even usually in good alignment, as I think they are in the case of static property requirements and cases without associated values. (I hope you and I agree that the hypothetical example I presented regarding cases for infinity and nan pose no such questions as you and I both have raised about the example regarding floating-point operations.)

1 Like

That’s not what I meant to imply :wink:. I just meant that it made sense to me for the requirements listed, but I hadn’t dug through the rest of the requirements to see if the conformance made sense. I think a better case against this conformance should show why those generic algorithms don’t make sense for operating on such a type, because showing the cases alone doesn’t convince me that it’s a bad fit.

Yep, agreed. Static properties seem like a clear fit.

Well, I think we can go about this argument in a slightly different, more straightforward way:

Current generic algorithms that operate on FloatingPoint (or rather more likely, BinaryFloatingPoint) can rely on the fact that the result of any of these operations is already worked out when the call to that operation returns.

This is because, given the requirement to return Self and the lack of value subtyping or this proposed feature, there are few if any ways (and certainly no straightforward ones) to create a hypothetical calculator tool today which conforms to these protocols. That these operations actually compute a result is pretty much a part of the semantics guaranteed by FloatingPoint.

One could design a protocol which explicitly doesn't make such a guarantee: imagine a protocol _PotentiallyLazyFloatingPoint { associatedtype Sum: Numeric; func +(Self, Self) -> Sum /* ... */ }. But our numeric protocols did not adopt this design.

In essence, then, the addition of the feature pitched here with respect to cases with associated values would retroactively weaken the semantic guarantees of existing protocols. If by "see[ing] if the conformance made sense" means asking if our hypothetical calculator type would be usable in currently written generic algorithms for FloatingPoint, we would have to answer this question in the negative in the general sense--although this may not be the case for any specific generic algorithm (depending on whether it relies on the "computed-ness" of the arithmetic result).

Therefore my argument that this is not a perfect semantic fit that we have between cases with associated values and static function requirements.

1 Like

Of course it does! I wouldn’t expect to find good examples of this being useful in the domain of numerical computing.

Many of the use cases where I've wanted this feature have been in the context of event handling. @technogen posted an example in this context upthread. Here's a slightly different variation based on a simplified version of some code I wrote recently:

protocol ActionProtocol {
    associatedtype ViewEvent
    associatedtype StateChange

    static func viewEvent(_ viewAction: ViewEvent) -> Self
    static func stateChange(_ stateChange: StateChange) -> Self
}

protocol ActionHandler {
    associatedtype Action: ActionProtocol
    mutating func handle(action: Action)
}

Types conforming to the ActionHandler protocol are values that are managed by a library runtime. In the above example, they must be able to handle view events and state changes, but may also handle additional kinds of actions specific to the hander type.

One option would be to place independent handler method requirements on the ActionHandler protocol, but for various reasons not relevant to the present discussion it is helpful to route everything through a single handler method using a single action type. In order to do this, the library runtime must be able to lift view events and state changes into an Action value.

In a system like this, Action types are predominantly enums. These enums often have cases with associated values. Sometimes, the library needs the ability to lift values into those cases (as describe above). The experience for users of a library like this will be significantly improved if the current pitch is accepted. The user can just write:

enum MyAction: ActionProtocol {
    case viewEvent(MyViewEvent)
    case stateChange(MyStateChange)
    // other actions
}

Currently, the user is required to write a static factory method explicitly and adjust the case name so it doesn't conflict:

enum MyAction: ActionProtocol {
    case _viewEvent(MyViewEvent)
    case _stateChange(MyStateChange)
    // other actions

   static func viewEvent(_ event: MyViewEvent) -> Self { ._viewEvent(event) }
   static func stateChange(_ change: MyStateChange) -> Self { ._stateChange(change) }
}

Or the library design needs to be adjusted to use a lifting initializer instead of a factory method:

protocol ActionProtocol {
    associatedtype ViewEvent
    associatedtype StateChange

    init(_ viewAction: ViewEvent)
    init(_ stateChange: StateChange)
}

So the user code looks like this:

enum MyAction: ActionProtocol {
    case _viewEvent(MyViewEvent)
    case _stateChange(MyStateChange)
    // other actions

   init(_ event: MyViewEvent) { self = .viewEvent(event) }
   init(_ change: MyStateChange) { self = .stateChange(change) }
}

In the library this example is inspired by this is what I did in order to avoid compromising the case name in the user's enum. Fortunately in the use case I am discussing this approach was acceptable. In other contexts this tradeoff may be less acceptable. Yet forcing the user's type to compromise its case name is pretty gross.

The best solution to this problem is to accept this proposal. As with any feature, it will be relevant in some domains and not relevant in others. This feature happens to be extremely relevant to the domain of app development, which is the domain in which Swift is used most frequently (for now at least). So I think the case for adding this feature is very strong.

1 Like

Thanks for the example. I think we've found agreement that cases with associated values are semantically good matches for static requirements which have the semantics of factories. This is a distinct subset of static function requirements returning Self, though granted in various domains it may be a near-nonexistent minority or a large majority.

[Edit: The counterargument here would be some that all static function requirements returning Self have factory semantics and in practice can be used as such; I think the FloatingPoint example demonstrates some issues with such, but perhaps I am wrong.]

But this is very different, I think, from the case of static properties and cases without associated values, which match semantically in essentially all examples we've explored. (Would love to see counterexamples where it's clearly not the case.)

I agree that what is pitched here is a solution to improve the problem you've laid out. But I would argue that the best solution to this problem would be a way to designate certain static functions (or at least their requirements) as factories. Such a semantic annotation would allow for the more "slam dunk" match that we have between cases without associated values and static property requirements to be extended to cases with associated values.

Whatever the way forward, I hope that you and I can agree that there is this qualitative difference, then, between the part of the pitch tackling static property requirements and that tackling static function requirements.

2 Likes

As a hypothetical, this could be a fully-fledged @implements feature which simply allows case statements with associated values to implement init requirements and static func requirements that return Self, without allowing bare case statements (without @implements to satisfy those static func requirements.

1 Like

The flip side, of course, is that there is no mechanism to prevent syntactically valid non-enum types from conforming to semantically inappropriate protocols, such as a linked list type confirming to Collection without documenting a non-O(1) subscript time complexity. Semantics are up to protocol authors to document, and clients to read and verify before conforming their type. It seems a bit strange to insist on a further syntactic barrier to conforming to semantically invalid protocols when no such barrier exists outside of enum types.

7 Likes

I randomly had the .swiftinterface file for Combine framework open, because I looked up something previously and for one reason the page stopped around this particular protocol which just caught my attention as it seems that the late discussion is about exactly this type of protocols.

public protocol SchedulerTimeIntervalConvertible {
  static func seconds(_ s: Swift.Int) -> Self
  static func seconds(_ s: Swift.Double) -> Self
  static func milliseconds(_ ms: Swift.Int) -> Self
  static func microseconds(_ us: Swift.Int) -> Self
  static func nanoseconds(_ ns: Swift.Int) -> Self
}
7 Likes

If you download the toolchain, you can actually make DispatchTimeInterval retroactively conform to it, which is what I believe Apple might have preferred to do instead if this feature was available. Right now, there's a different type (DispatchQueue.SchedulerTimeType.Stride) that conforms to it and accepts a DispatchTimeInterval instead.