I'm experimenting with some strategies to express an "empty" generic constraint. FWIW my focus is mostly on generic types for now… but I might also use this advice to think creatively about generic functions.
I have some code written on parameter packs. Here is a variadic type:
struct Repeater<each T> {
init(_: repeat each T) {
}
}
let _ = Repeater(1, 2, 3, "Repeater!")
let _ = Repeater("Repeater!")
let _ = Repeater()
What's cool about Repeater is that the semantics at the call sites are clean and easy for expressing multiple constraints, one constraint, or no constraints.
I'm looking for something similarly "clean" for "classic" generic types. Here is a generic type across one value, which might be nil:
struct S1<T> {
init(_: T? = nil) {
}
}
let _ = S1("S1")
let _ = S1<Never>()
This mostly works… except for the explicit Never that needs to be there to keep the compiler happy.
Here is another approach:
struct S2<T> {
init(_: T) {
}
init() where T == Never {
}
}
let _ = S2("S2")
let _ = S2()
Here we require the value passed to init to not be nil… but we define another constructor that takes no value and constraints T to Never. This enables me to call S2() without an explicit Never.
I feel like I'm conceptually searching for something in a similar spirit to the support for "empty" packs… but for classic generic types. Is there anything in the 6.0 compiler that can support this? Are there any tricks to improve the semantics of S1 or S2 to indicate that callers can construct instances "without any T"?
Hmm… This sounds like one possible idea. I'm not totally opposed to existential types… but after this type would be constructed there's nothing happening internally that "needs" this type to be erased. I'm actually using the type T to constrain more implementation logic after construction. This could get me the semantics at the call site I was looking for… but AFAIK I would also be paying a small performance penalty for the existential?
The idea you use in S2 is perfectly fine for me. The user of S2 should not be worried about the actual generic parameter, only the interface you provided matters.
Personally, I would use Void instead of Never as the default type parameter though.
I doubt if the approach is S2 is useful in practice. Let's suppose the type parameter is used as the type of a stored property. That stored property will be accessed in most methods of S2 (otherwise S2 doesn't need to be a generic type). That means all these methods will need to check if T is Never and handle it separately. This will be tedious and hurt code readability a lot.
In my understanding generic type is intended to express "any type", it's not suitable to express "a type that may or may not exist". Variadic generic is different because IMO it's like a container. A container with nothing is still a container.
The problem I run into there is this type T might have constraints:
struct S2<T> {
init(_: T) {
}
init() where T == Void {
}
}
struct S3<T: Equatable> {
init(_: T) {
}
init() where T == Void {
`- error: no type for 'T' can satisfy both 'T == ()' and 'T : Equatable'
}
}
Constraining my default type to Never compiles correctly without an error.
Are there more benefits to using Void here that would be important to know about?
This type itself is kind of a funny type… it's an attempt to "backport" a subset of functionality from a type that is variadic generic to platforms that do not support variadic types (like macOS 13). So the semantics of the variadic generic do support an "optional" T that could be "missing-slash-empty".
Now I understand why you choose Never. That totally makes sense.
The reason why I considered Void in the first place is just this: if this T appears in other interfaces, the specialized implementation for T == Void becomes more natural, because we can just create an instance of Void, which is not possible for Never.
I took this idea from some first party libraries. For example PassthroughSubject, some of its APIs has an overload for T == Void.
I'm working on some code that builds off this idea of using Never for an "empty" generic constraint. This is a constraint that adopts a custom protocol that I control. Adopting that protocol on Never seems to be working fine.
My question is what are my options for adopting the protocol on Never. Here is an example:
protocol P<T> {
associatedtype T
func f1()
func f2() throws
func f3() -> T
func f4() throws -> T
}
This compiles with no errors or warnings. The documentation for Never tells us that instances of Never cannot be constructed. It looks like fatalError here is not going to be a problem.
A different option is for Never to either just return or throw a Swift.Error. This seems to be what happens in the Never conformances to Encodable and Decodable:
Is this a matter of personal style… or are there some deeper insights and philosophy here? Is there any reason why a function like this would run on Never? Would that imply a programmer error? Would that imply some kind of naturally occurring app state that would mean this would need to be a recoverable error?
switch self {} is a quick, if obscure, way to say “there are no values of this type, the compiler has checked that for me, and therefore this code is unreachable (unless someone has already broken the rules)”.
However, the compiler doesn’t seem to be consistent about it—if the Never is an argument rather than self, there’s an unreachable code warning.
Hmm… that trick works when the protocol requirements are instance methods on Never… but breaks when the protocol requirements are static:
protocol P<T> {
associatedtype T
static func f() throws -> T
}
extension Never: P {
static func f() throws -> Never {
switch self {}
// error: 'switch' statement body must have at least one 'case' or 'default' block; add a default case
}
}
extension Never: P {
static func f() throws -> Never { .init() }
}
(Never is special in that there are actually functions that "return it", like fatalError. The above form is the only option I know of for other uninhabited types.)
enum E {
init() { switch self { } }
static var instance: Self { .init() }
}
You can call static methods on Never, so you actually have to provide a proper implementation for them. (Consider Never.allCases, surely it should return an empty array!)
Hmm… maybe that kind of transitions back to my old question… whether or not Never can be made to fit in with this idea of an "empty" constraint might not necessarily make it an idiomatic convention. From what I have seen so far Never might be the right choice in terms of any potential tradeoffs… but I'm open to continuing to brainstorm other options.
Even “empty” constraints have requirements, just like they can have associated types (which are themselves probably going to be Never, but you have to say that). There’s nothing you can really do to get around that unless you check at every call site whether you’re in the “empty” case, and Swift is not (currently?) a language that can enforce you’ve done that. You could imagine a language that has generic parameters that may be nil (or Nil? ), but for Swift you have to put something in the slot.
I feel like if the concept of the "empty parameter pack" was somehow back-ported to work as a legit non-variadic generic constraint that might be what I'm looking for. I guess for now Never might work for me.
Could you think of any major blockers for a pitch or proposal to eventually attempt and "backport" the concept of an empty parameter pack as a variadic generic constraint and ship some kind of legit empty type that can function as a non-variadic generic constrain as an alternative to using Never like this example?