This is an interesting problem. What should happen if that function gets called when no one supplied the conformance? Right now it would silently "fail" (skip over the S
s). Should it instead terminate the program?
I suppose the API of the module would ideally require someone to provide a conformance of S
to Identifiable
, but there's no way to do that in Swift. The closest we can get is adding a global hook that starts off failing at runtime (as in fatalError
) with an instruction to replace it.
Since Identifiable
has an associated type, and this hook implies the implementation (including the type of the ID that gets returned) is now set at runtime, you lose type safety and just fall back to type erased IDs (luckily the specially treated AnyHashable
eraser makes this easier).
public var getSID: (_ s: S) -> any Hashable = {
fatalError("You must supply a definition for the ID of an `S`")
}
public struct S: Identifiable {
var id: AnyHashable { getSID(self) }
...
}
I have wanted the ability to leave unimplemented hooks like this that some other module must define many times. This is possible in C/C++ by supplying a header declaration for a function in a library (so the library implementation can include and call it) but not a definition. If the library is statically linked, the library compiles fine but once you include the library in an executable (either a program or a dylib), if you don't define the function you are hit with an undefined symbol error at link time. This is perfect, as long as you understand the error. It catches the mistake of not defining the function at build time.
If the library is dynamically linked, you need to turn on the compiler option to allow undefined symbols, and the error of not defining the function gets pushed to runtime as soon as the library gets loaded. This is less ideal, but if you truly need dynamic linkage (you usually don't, it's needed for e.g. a plugin system) this is the only way that makes sense. At least the error occurs as soon as the library is loaded and not whenever the function is first called.
Allowing Swift to do something like this would require this ability to wire up calls at link time, which enables link time polymorphism. I pitched what I believe is a more "Swifty" (instead of C-like) way to accomplish this a while ago here.
Another approach is to do this with generics. You can maintain strongly typed IDs that way:
protocol SIDProvider<ID> {
associated type ID: Hashable
static func id(for s: S<Self>) -> ID
}
struct S<IDProvider: SIDProvider>: Identifiable {
var id: IDProvider.ID { IDProvider.id(for: self) }
...
}
public
func f<IDProvider: SIDProvider>(values:[any Identifiable<IDProvider.ID>])
{
for case let value as S<IDProvider> in values
{
}
}
Now all the code up until the module that defines the ID needs to become generic, and by propagating the same type parameter this expresses that all the code works with a single ID Provider. Once the module defining the ID is present, you just assign that type parameter to the SIDProvider
implementation defined in that module and everything above it is no longer generic. This expresses that this is where the ID definition gets locked down.
This is how I would solve this problem. It's more "invasive", lots of stuff (everything between where S
enters the picture and the module defining the ID comes in) has to be modified but it's strongly typed and fully compile time enforced.
Another example where generics proliferate, which I found scary at first but eventually got used to it and now favor it and exercise them frequently and pervasively.