I'm not sure there's any way to make this work with two different types from the top level namespace that are compiled together but are "unique" across runtimes.
The ideal callsite would be something like this for macOS 14:
let _ = S(1)
let _ = S(1, 2)
let _ = S(1, 2, 3)
With users on older systems running the "legacy" version:
let _ = S(1)
let _ = S(2)
let _ = S(3)
I don't think this is possible using the existing tools to express availability. AFAIK my option at this point is to explicitly name these types differently (S1 and S2)… with the product engineer then needing to choose the appropriate type with some logic at runtime.
Any other ideas or case studies about this situation? Two types with the same name coexisting at compile time but independent across runtimes?
A possible solution would be to use compiler control statements. The downside to using them is they might not give you the exact control you want and can be tedious to use if you need a lot of them, but you can still achieve it doing some research and testing.
Unfortunately this only works if your deployment target is at least macOS 14. Try this instead, you'll see an error:
@available(macOS 200, *)
struct S { }
enum Storage {
@available(macOS 200, *)
case foo(S)
}
error: enum cases with associated values cannot be marked potentially unavailable with '@available'
There's no mechanism to perform dynamic layout based on availability at runtime; enum cases and stored properties cannot involve a less-available type than the enum or struct itself.
That's an interesting idea. It might be even cleaner to define a new protocol that S (and only S) conforms to, and then you can avoid the cast and just store an any P instead.
Hmm… so we're thinking something like a Box wrapper? An abstraction indirection layer that wraps an existential type that can be resolved at runtime?
struct Box {
@available(macOS 14.0.0, *)
init<each T>(_: repeat each T) {
}
init<T>(_: T) {
}
}
@available(macOS 14.0.0, *)
func f() {
let _ = Box(1)
let _ = Box(1, 2)
let _ = Box(1, 2, 3)
}
let _ = Box(1)
let _ = Box(2)
let _ = Box(3)
This compiles with no errors. Maybe my question at that point might be if the user is running on macOS 14 and both constructors are available… is there a stable and deterministic pattern to know which constructor is chosen by the compiler when T is just one element? Can I expect the compiler to treat T as a "parameter pack of one"?
And then there is also:
struct S {
init(_: Any...) {
}
}
let _ = S(1)
let _ = S(1, 2)
let _ = S(1, 2, 3)
Which can just forget about parameter packs and modern variadic types in the name of supporting legacy platforms without additional plumbing implementation.