Access control for enum cases

There are two aspects to this:

  1. Adding default:: If you are using a framework with library evolution enabled today, then you already need to add default: cases for non-frozen enums. This pitch is using the same restriction, it's not an additional complication.
  2. Non-public cases in interfaces: Apologies for not spelling this out in the pitch. My current thinking is that:
    a. For interfaces that are generated from swiftmodules (which would be the case for libraries compiled without library evolution), we would display non-public cases to avoid confusion where someone is like "why can't I exhaustively match on this enum even though the generated interface tells me that I've covered all cases?"
    b. For frozen enums (in the presence of library evolution), non-public cases will be emitted into the compiler-synthesized swiftinterface, and could be shown inside an editor to avoid confusion (as above).
    c. For non-frozen enums (in the presence of library evolution), cases are omitted from the compiler-synthesized swiftinterface, so it is not possible to "recover" non-public cases.

The associated values of an enum case form, in essence, an unnamed struct. So you can have certain invariants which need to be enforced between different associated values, analogous to invariants involving different properties of a struct. In the rdar://22891351: Swift: Private enum cases I linked earlier, there's a concrete example of a situation where this comes up in practice.

I think enum inheritance is somewhat of an orthogonal issue to access control. Today, the language supports both access control and inheritance for classes; they serve different purposes. Access control is about "where can I (not) access X from", inheritance is about extending types. Given some of the positive responses in the thread by Matthew, I suspect other people also have use cases where they would like to be able to enforce invariants by limiting the ability to construct certain enum cases.

Is the problem of being able to easily extend enums worth solving? Sure, I agree. However, I think that pivoting this pitch to instead implement inheritance for enums is not a good idea for the following reasons:

  1. The semantics are different in the absence of library evolution: If I have an internal case, changing that to public does not stop client code from compiling, because they can't be relying on exhaustivity. However, if one uses enum inheritance, then adding a new case means that clients will stop compiling because they were (by design) relying on exhaustivity.

  2. I suspect that designing and implementing enum inheritance will be significantly more complicated compared to implementing access control for cases (you can check the current prototype, it's not that big of a change). What you've described is part of the design that needs to be thought through, but there's more. Some examples of complications:

    • How should name collisions in case names be handled across library boundaries?
    • Library evolution is potentially a can of worms:
      • Can you "sink cases" across an inheritance hierarchy? At runtime, a client may need to get the discriminant based on the mangled symbol for the case. Changing which type contains a case would change its mangling which would make this a breaking change; should we add a way to spell that "this case should be mangled as if it were defined in this other type"?
      • How does layout for non-frozen enums (generally boxed today) inheriting from frozen enums (unboxed) work? Do we disallow that? Do we add implicit boxing (similar to creating an existential from a concrete type)? Something else?

    The gist of what I'm trying to get at here is that: inheritance is complicated. I'm not saying it's impossible, but it's a hard problem. In contrast, access control for cases, we can answer most questions by asking "what happens with stored properties" or "what happens for (non-)frozen enums in library evolution". With inheritance, we also have less prior art (amongst mainstream languages, I only know of Scala which has similar functionality).

  3. Encoding access control indirectly using multiple types defeats the purpose of this pitch; this pitch is trying avoid encoding access control as something else. Today, you can already emulate similar behavior with structs to some extent. Exchanging one kind of emulation for a slightly better emulation is not the goal.


Yeah, I originally thought of this, but I wasn't entirely sure if it should go one way or another... I was thinking maybe there's an inconsistency in pattern-matching inside and outside the module, which felt a bit odd. But I think this is a reasonable alternative to the semantics I've proposed and I wouldn't be opposed to doing this.

Well, I may have skipped a few details. What I meant by the "swiftinterfaces are only supported" comment was in terms of "what is consumed by tools". The generated swiftinterface in Xcode is "not supported" in the sense that it is "not supported [for consumption by tools]", it's for people, so we are generally free to change it without breaking anyone. For this reason, I didn't speak about generated interfaces, but I see that was a mistake. :sweat_smile: I've corrected that in an earlier part of the comment in my response to James. Hope that clarifies things.

1 Like