Skip to content

Instantly share code, notes, and snippets.

@BigZaphod
Created December 10, 2022 20:21
Show Gist options
  • Save BigZaphod/b47fec45ec5e053a7a9c39420b0d25b7 to your computer and use it in GitHub Desktop.
Save BigZaphod/b47fec45ec5e053a7a9c39420b0d25b7 to your computer and use it in GitHub Desktop.
// The original Container protocol.
protocol Container {
associatedtype Item
func deleteItem(_: Item)
}
// Let's make a couple of concrete containers that use our protocol:
class StringContainer: Container {
typealias Item = String
func deleteItem(_ item: String) {
// `item` is guarenteed to be a `String` instance here
}
}
class IntContainer: Container {
typealias Item = Int
func deleteItem(_ item: Item) {
// `item` is guarenteed to be a `Int` instance here
}
}
// If the Service's API is simple and concrete, we can do things pretty easily:
struct StringService {
let container: StringContainer
// This accepts a `String` instance directly since we know `container` is always a `StringContainer`
// so we don't need to deal with anything else or do any casting.
func deleteItem(_ item: String) {
container.deleteItem(item)
}
}
// If we need multiple instances of the service using different types of containers, we could use generics.
// This makes sure that the compiler knows exactly which types we need in each context it is used in:
struct ItemService<C: Container, I> where C.Item == I {
let container: C
// This accepts any item of type `I` which is guarenteed by the `where` rule above to match the container's
// `Item` type. So we have no problem compiling here.
func deleteItem(_ item: I) {
container.deleteItem(item)
}
}
// But when we try to be super generic and allow the service to any type of Container, then the container's
// Item type could also be any type of value. Now we start getting confused becuase we don't know at compile
// time which types are which:
struct AbstractService {
let container: any Container
// Since `container` is `any Container`, the actual container's associated type could be ANY type at all
// and we have no way to know what it is because it depends on what *instance* of a container we assign
// to the container property. It could be a StringContainer.. it could be an IntContainer.. or it could
// be a `FooContainer` or `BazContainer`... or who knows!
func deleteItem(_ item: Any) {
// We can't figure out if the type `container` expects is the same type we were given. :(
// container.deleteItem(item) --- doesn't compile
}
}
// The core problem here, I think, is that the information the compiler needs to know is hidden inside of the
// container's type and Swift doesn't have a reasonable way to get it out (without the newer primary associated
// type feature, I guess).
//
// So, how can we make it available ourselves?
//
// If the container itself knows, then maybe we can just move the casting to the container! This is easy to do
// with helper function added to the Container protocol:
extension Container {
func deleteAnyItem(_ item: Any) {
if let castItem = item as? Item {
deleteItem(castItem)
}
}
}
// And now we can do what is needed, I think?
struct AlternateAbstractService {
let container: any Container
func deleteItem(_ item: Any) {
container.deleteAnyItem(item)
}
}
// Note that this could instead be written with the generic syntax instead of `Any`, but since it eventually has
// to do a runtime cast, I'm not entirely sure it buys us anything. Perhaps the compiler could sometimes optimize
// things a tiny bit more this way, though:
extension Container {
func deleteSomeItem<I>(_ item: I) {
if let castItem = item as? Item {
deleteItem(castItem)
}
}
}
struct GenericAbstractService {
let container: any Container
func deleteItem<I>(_ item: I) {
container.deleteAnyItem(item)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment