Hi everyone,
We are in the process of implementing a feature using a property wrapper around a generic WrappedValue
protocol that functions similarly to the SwiftUI Environment: The user defines a type, and we inject an instance conforming to the Constraint
protocol (simplified here only to indicate that it should be an actor) in the property wrapper at runtime using the Mirror
API.
We use the inject
function to inject the value in the property wrapper and want to check that it is actually conforming to the WrappedValue
protocol. This requirement is currently checked at runtime in the inject
method (another issue where we currently have to rely on a runtime check, but that could be its separate topic):
protocol Constraint: Actor {}
@propertyWrapper
struct PropertyWrapper<WrappedValue: Constraint> {
var wrappedValue: WrappedValue { fatalError("Not implemented ...") }
init(_ constraint: WrappedValue.Type = WrappedValue.self) {
if isConstraint(constraint) {
print("Fulfills constraint!")
} else {
print("Does not fultill constraint!")
}
}
func inject<C: Constraint>(constraint: C) {
guard let constraint = constraint as? WrappedValue else {
fatalError("The Constraint instance does not conform to \(String(describing: WrappedValue.self))")
}
// ...
}
}
Unfortunately, we are having trouble constraining the WrappedValue
to conform to the Constraint
protocol when using existentials, as shown in the following example:
actor TestClassConformingToConstraint: Constraint {}
protocol TestProtocolConformingToConstraint: Constraint {}
protocol TestProtocolNotConformingToConstraint {}
struct ExampleA {
/// Prints: Fulfills constraint!
@PropertyWrapper
var exampleA1: TestClassConformingToConstraint
/// Does not compile: Type 'any TestProtocolConformingToConstraint' cannot conform to 'Constraint'
// @PropertyWrapper
// var exampleA2: any TestProtocolConformingToConstraint
/// Does not compile: Type 'any TestProtocolConformingToConstraint' cannot conform to 'Constraint'
// @PropertyWrapper
// var exampleA3: any TestProtocolNotConformingToConstraint
}
let exampleA = ExampleA()
Unfortunately, we have not found any way to use existential without removing the generic type constraint, as shown in the following example. Nevertheless, it seems important that we enable this type of expressiveness as we can not rely on inheritance when using actors in this context to protocol extensions seem to be the only way to express additional constraints on the injected type.
Any input on enabling this functionality while enforcing that constraint is greatly appreciated!
An alternative we considered was using an approach that does not provide the type safety we would like to have at compile time but would at least allow us to use preconditions or fatal errors to warn a user of a misuse of the API at runtime.
We have found Test if a type conforms to a non-existential protocol as a great thread on that topic, but the approach described there does unfortunately not work and does not enable the runtime check we would expect from this code:
protocol ConformsToConstraint {}
enum ConstraintMarker<T> {}
extension ConstraintMarker: ConformsToConstraint where T: Constraint {}
func isConstraint<T>(_ t: T.Type) -> Bool {
return ConstraintMarker<T>.self is ConformsToConstraint.Type
}
@propertyWrapper
struct PropertyWrapperCheckingAtRuntime<WrappedValue> {
var wrappedValue: WrappedValue { fatalError("Not implemented ...") }
init(_ constraint: WrappedValue.Type = WrappedValue.self) {
if isConstraint(constraint) {
print("Fulfills constraint!")
} else {
print("Does not fultill constraint!")
}
}
func inject<C: Constraint>(constraint: C) {
guard let constraint = constraint as? WrappedValue else {
fatalError("The Constraint instance does not conform to \(String(describing: WrappedValue.self))")
}
// ...
}
}
struct ExampleB {
/// Prints: Fulfills constraint!
@PropertyWrapperCheckingAtRuntime
var exampleB1: TestClassConformingToConstraint
/// Prints: Does not fultill constraint!
@PropertyWrapperCheckingAtRuntime
var exampleB2: any TestProtocolConformingToConstraint
/// Prints: Does not fultill constraint!
@PropertyWrapperCheckingAtRuntime
var exampleB3: any TestProtocolNotConformingToConstraint
}
let exampleB = ExampleB()
It would be amazing to hear if we missed anything in exploring this topic or if someone has an idea of how to best address this issue. Maybe we have missed an other topic that discusses some possible approaches?
We would be happy to test this requirement at runtime if this is not possible at compile time, so any input on the second solution would also already help us a lot. Any input and support is greatly appreciated!