aurelienp
(Aurélien Porte)
1
Questions are:
- how to type-erase a PAT (a Protocol with Associated-Type) that exposes an init method? The PAT being
SpecializedContribution and my tentative being AnySpecializedContribution in the code example below.
- how to type-erase a PAT that exposes the metatype of the associatedtype? The PAT being
SpecializedContributionConfiguration and my tentative being AnySpecializedContributionConfiguration in the code example below.
I realized this is 2 questions instead of 1 but I have the intuition that they are tied together and could both be resolved by playing with metatypes.
The idea of the code below is to allow sub-modules to vend "contribution configurations" to the main-app module but to let the main-app module actually init the contributions. This is notable useful to enforce, at compile time, that sub-modules don't cheat and rely on contribution singletons internally.
public protocol Contributing {
associatedtype Dependencies
init(dependencies: Dependencies)
}
public protocol ContributionConfigurating {
associatedtype Contribution: Contributing
var type: Contribution.Type { get }
var dependencies: Contribution.Dependencies { get }
}
public protocol SpecializedContribution: Contributing {
var anAPI: Int { get }
}
public protocol SpecializedContributionConfiguration: ContributionConfigurating where Contribution: SpecializedContribution {}
// If I build a "configuration provider" that returns a single configuration, it works like a charm
// thanks to opaques types. But I want the provider to return multiple configurations.
public protocol SingleSpecializedContributionConfigurationProviding {
associatedtype ConcreteContributionConfiguration: SpecializedContributionConfiguration
func contributionConfiguration() -> ConcreteContributionConfiguration
}
class SingleSpecializedContributionConfigurationProvider: SingleSpecializedContributionConfigurationProviding {
public func contributionConfiguration() -> some SpecializedContributionConfiguration {
// <code that actually returns a concrete implementation of a SpecializedContributionConfiguration>
}
}
// First attempt, just return the protocol. This doesn't work, a classic array-with-PATs issue.
// No surprise so far.
public protocol MultipleSpecializedContributionConfigurationsProviding {
func contributionConfigurations() -> [SpecializedContributionConfiguration]
}
// Second (and current) attempt, type-erase the configurations.
// Problems?
// 1/ The configuration protocol vends a metatype.
// 2/ The contribution protocol, which I'd have to type-erase eventually, exposes an init method.
public protocol TypeErasedMultipleSpecializedContributionConfigurationsProviding {
associatedtype ConcreteContributionConfiguration: SpecializedContributionConfiguration
func contributionConfigurations() -> [ConcreteContributionConfiguration]
}
class TypeErasedMultipleSpecializedContributionConfigurationsProvider: TypeErasedMultipleSpecializedContributionConfigurationsProviding {
public func contributionConfigurations() -> [AnySpecializedContributionConfiguration] {
[
AnySpecializedContributionConfiguration(
// Let's assume this is defined somewhere
FooContributionConfiguration()
),
AnySpecializedContributionConfiguration(
// Let's assume this is also defined somewhere
BarContributionConfiguration()
)
]
}
}
public struct AnySpecializedContributionConfiguration: SpecializedContributionConfiguration {
public typealias Contribution = AnySpecializedContribution
public var type: Contribution.Type {
// I don't know what to store here.
AnyContribution.self
}
public var dependencies: Contribution.Dependencies {
_dependencies()
}
private let _dependencies: () -> Contribution.Dependencies
init<T: SpecializedContributionConfiguration>(_ configuration: T) {
_dependencies = {
configuration.dependencies
}
}
}
public struct AnySpecializedContribution: SpecializedContribution {
public typealias Dependencies = Any
private let _anAPI: () -> Int
public var anAPI: Int {
_anAPI()
}
init<T: SpecializedContribution>(_ contribution: T) {
_anAPI = {
contribution.anAPI
}
}
// How to "type-erase the Contributing.init(dependencies: Dependencies)"?
}
aurelienp
(Aurélien Porte)
2
My understanding is that I'm basically trying to fight against the current lack of generalized existentials, by relying on type-erasure. But the type-erasure pattern seems to presents its own limit, leaving me in an uncomfortable situation.
From there, I wonder if:
- I should stick in this type-erasure direction, but then how?
- I should explore a different work-around to the lack of generalized existentials?
I would try to split off the init() requirement into it's own protocol to which the type eraser doesn't conform. Otherwise, you could construct an instance of some default type.
I don't think there is a way if you want the type-erased metatype to conform to some protocol bound that itself has static (or init) requirements. In your example, var type: Contribution.Type has to be implemented in the type eraser by returning AnySpecializedContribution.self. However a struct metatype carries no state, so the static methods on that metatype have to be implemented without knowing the original concrete type that was erased.
If your protocol was instead a factory, like
public protocol ContributingFactory {
associatedtype InstanceType: Contributing
func construct(with: InstanceType.Dependencies) -> InstanceType
}
Then an AnyContributingFactory instance could carry the erased type information.
aurelienp
(Aurélien Porte)
4
Thanks a lot for your prompt answer @Slava_Pestov.
Being aware of the rest of the interface (such as SpecializedContribution.anAPI) is a good thing, but being aware of the init itself is also quite important in my case. Indeed, the main-app module will receive the Contributing types and be in charge of instantiating them.
In a world where I go for a bit of type-unsafety, your suggestion is safer than just using Any so this is a great step already, thank you.
Sorry, I'm not sure I understand what you mean here.
If your protocol was instead a factory
If I understand your suggestion well, doesn't it defeat the purpose of this whole architecture, which is for sub-modules to "delegate" the instanciation of contributions to the main-app module to avoid that sub-modules feel like returning contribution singletons. Indeed, if it's a sub-module responsibility to implement the construct method, they can return any instance they want, including a singleton.