Introduction
Today, Swift doesn't allow access control modifiers on enum cases, unlike properties, methods and other kinds of declarations. This proposal fixes that hole in the language, making it more uniform.
Motivation
Swift doesn't allow access control modifiers on enum cases. This means that when one wants to selectively expose enum cases, or forbid clients from constructing certain enum cases, one needs to resort to exposing the API as a struct, which may be internally implemented as a wrapper around an enum. One then defines static properties/methods on the struct to mimic the ability to construct and access the "public cases" of the enum.
Such an API poses:
- Poorer usability as one cannot pattern match while binding associated values (pattern matching still works for cases without associated values).
- Poorer discoverability for clients which need to understand that the type actually represents some mutually exclusive cases.
- An unnecessary maintenance burden on maintainers.
Proposed Solution
We allow access control on enum cases. (Diagnostic text is a placeholder for a proper diagnostic.)
// module M
public enum A {
case x
internal case y
}
// module N
import M
func f(_ a: A) {
switch a {
case .x: print("x")
case .y: print("y") // error: 'y' is inaccessible due to 'internal' protection level
}
}
let _ = A.y // error: 'y' is inaccessible due to 'internal' protection level
Based on the familiar rules of access control, the case y
is only accessible within the module M
which defines A
, an inaccessible from any other modules. This means two things:
- When a downstream module tried to switch on a value of type
A
, it needs to use adefault:
clause, asy
is not accessible for pattern matching. - A downstream module cannot construct
y
.
Analogous to private(set)
and similar, we also allow private(init)
to allow clients to match on a case but forbid them from directly initializing it.
// Module P
public enum B {
case v
internal(init) case w
}
// Module Q
import P
func f(_ b: B) {
switch b {
case .v: print("B.v")
case .w: print("B.w") // OK
}
}
let _ = B.w // error: 'y' is inaccessible due to 'internal(init)' protection level
Detailed Design
(This section will be more fleshed out in the full proposal.)
Library Evolution
The semantics of non-public enum cases for frozen enums are similar to those for non-public stored properties:
- They are allowed: Changing a non-public case to a public one would be a binary-compatible and source-compatible change.
- They are exposed in the swiftinterface for layout purposes: This requires that types for any associated values must be
@usableFromInline
or public.
Conformance synthesis
Codable
synthesis is supported for enums with non-public cases. For precedent, Codable
synthesis is supported today for structs with private stored properties.
CaseIterable
and RawRepresentable
synthesis is not supported for enums with non-public cases, since allCases
and init?(rawValue:)
would allow one to easily create non-public cases.
Why the difference between Codable
and CaseIterable
/RawRepresentable
?
-
Codable
is quite central and used in many APIs, so not havingCodable
synthesis would be a more onerous burden. Writing a conformance toCodable
is generally more work than writing a conformance forCaseIterable
orRawRepresentable
. - It is more cumbersome (but not terribly difficult) to create a non-public case using
Decodable
'sinit(from:)
compared to usingallCases
orinit?(rawValue:)
.
Defaulting
@unknown
default will emit a warning only if all public cases not matched. (Today, all cases of a public enum are public, so the additional "public" in "public cases" is unnecessary.)
Alternatives considered
Only support access control for initialization
Matthew Johnson previously pitched private(init)
without adding full access control for enum cases. Implementing the additional support for controlling pattern-matching is not too complicated, so we think it's better to have a full-fledged feature rather than only private(init)
.
Today, it is surprising that access control is not supported for enum cases; changing that to "you can only restrict the access control of initialization" seems like a somewhat arbitrary restriction. Rather, it is nicer to lift the restriction entirely.
Allow conformance synthesis for CaseIterable and RawRepresentable
We could do this. In case a library author carelessly slaps : CaseIterable
(or : RawRepresentable
) on a public enum with private cases, disabling synthesis means that the author is forced to think a bit more carefully about what they are actually trying to achieve. I think that's potentially useful. However, there is a reasonable argument to be made that this imposes an unnecessary burden on the library author, especially since the language doesn't have a spelling for @I_know_what_I'm_doing_so_please_synthesize_this
.
Prototype
An initial prototype implementation is available here; you can see some code "in action" in a test case. At the time of writing, all the functionality isn't implemented (in particular, private(init)
and restrictions on conformances are missing), but the core functionality of access control with private case
(and similar) does work, including for initialization and pattern matching. I'm looking for feedback before proceeding.