To answer my own question, below is a solution to the above specific example. Note it avoids conformance check at runtime.
func test<T: ProductProtocol>(_ product: T) {
print("The product has no feature")
}
func test<T: ProductWithFeatureProtocol>(_ product: T) {
print("The product has feature: \(product.feature.id)")
}
let productA = ProductA(feature: FeatureA(id: UUID()))
let productB = ProductB()
test(productA)
test(productB)
But what I really wanted to figure out is if there is a way to check a value conform to a protocol with associated type, because there are scenarios where this approach is infeasible. Below is an complete example:
protocol ProductProtocol {
}
protocol FeatureProtocol {
associatedtype IDType: Equatable
var id: IDType { get }
}
protocol ProductWithFeatureProtocol: ProductProtocol {
associatedtype FeatureType: FeatureProtocol
var feature: FeatureType { get }
}
// Product A: it has a variation of the feature.
protocol FeatureAProtocol: FeatureProtocol where IDType: Hashable {
}
protocol ProductAProtocol: ProductWithFeatureProtocol where FeatureType: FeatureAProtocol {
}
struct FeatureA: FeatureAProtocol, Codable {
var id: UUID
}
struct ProductA: ProductAProtocol, Codable {
var feature: FeatureA
}
// Product B: it doesn't have the feature.
struct ProductB: ProductProtocol, Codable {
}
// API
func test<T: ProductProtocol>(_ product: T) {
print("The product has no feature")
}
func test<T: ProductWithFeatureProtocol>(_ product: T) {
print("The product has feature: \(product.feature.id)")
}
let productA = ProductA(feature: FeatureA(id: UUID()))
let productB = ProductB()
// This works.
test(productA)
test(productB)
// Setup: save productA and productB in an array of Codable. As a result, the compiler lost their original types.
var products: [Codable] = []
products.append(productA)
products.append(productB)
// Error: this doesn't compile.
for product in products {
test(product)
}
I think out two ways to get the above example working. Neither is ideal and both are based on the same idea: trying to keep (or restore) variable's original type. One approach is to cast the item retrieved from the array to its original type by checking it against all product variation types defined in the app. Another approach is to not mix up types when saving them in array, so it will be easier to cast the items to their original types. Below is an example of the second approach:
var productAs: [Codable] = []
productAs.append(productA)
var productBs: [Codable] = []
productBs.append(productB)
for product in productAs {
let product = product as! ProductA
test(product)
}
The approach also has a limitation: to access a value through a protocol which it conforms to and which has associated type, it has to be done through generic function or generic type. That means, if one just wants to access productA.feature.id
in above example, he can't use the dot syntax, instead he has to introduce a generic function just for this purpose. That seems very inconvenient to me.
Regarding the way to check if a value conform to a protocol with associated type, I find related discussions here and here. I didn't go through all the discussion, but it's obvious that Swift doesn't support it.