How do I add different types conforming to a protocol with an associated type to a collection?
As an exercise in learning I'm rewriting my validation library in Swift.
I have a ValidationRule
protocol that defines what individual rules should look like:
protocol ValidationRule {
typealias InputType
func validateInput(input: InputType) -> Bool
//...
}
The associated type InputType
defines the type of input to be validated (e.g String). It can be explicit or generic.
Here are two rules:
struct ValidationRuleLength: ValidationRule {
typealias InputType = String
//...
}
struct ValidationRuleCondition<T>: ValidationRule {
typealias InputType = T
// ...
}
Elsewhere, I have a function that validates an input with a collection of ValidationRule
s:
static func validate<R: ValidationRule>(input i: R.InputType, rules rs: [R]) -> ValidationResult {
let errors = rs.filter { !$0.validateInput(i) }.map { $0.failureMessage }
return errors.isEmpty ? .Valid : .Invalid(errors)
}
I thought this was going to work but the compiler disagrees.
In the following example, even though the input is a String, rule1
's InputType
is a String, and rule2
s InputType
is a String...
func testThatItCanEvaluateMultipleRules() {
let rule1 = ValidationRuleCondition<String>(failureMessage: "message1") { $0.characters.count > 0 }
let rule2 = ValidationRuleLength(min: 1, failureMessage: "message2")
let invalid = Validator.validate(input: "", rules: [rule1, rule2])
XCTAssertEqual(invalid, .Invalid(["message1", "message2"]))
}
... I'm getting extremely helpful error message:
_ is not convertible to ValidationRuleLength
which is cryptic but suggests that the types should be exactly equal?
So my question is... how do I append different types that all conform to a protocol with an associated type into a collection?
Unsure how to achieve what I'm attempting, or if it's even possible?
EDIT
Here's it is without context:
protocol Foo {
typealias FooType
func doSomething(thing: FooType)
}
class Bar<T>: Foo {
typealias FooType = T
func doSomething(thing: T) {
print(thing)
}
}
class Baz: Foo {
typealias FooType = String
func doSomething(thing: String) {
print(thing)
}
}
func doSomethingWithFoos<F: Foo>(thing: [F]) {
print(thing)
}
let bar = Bar<String>()
let baz = Baz()
let foos: [Foo] = [bar, baz]
doSomethingWithFoos(foos)
Here we get:
Protocol Foo can only be used as a generic constraint because it has Self or associated type requirements.
I understand that. What I need to say is something like:
doSomethingWithFoos<F: Foo where F.FooType == F.FooType>(thing: [F]) {
}
Solution 1:
Protocols with type aliases cannot be used this way. Swift doesn't have a way to talk directly about meta-types like ValidationRule
or Array
. You can only deal with instantiations like ValidationRule where...
or Array<String>
. With typealiases, there's no way to get there directly. So we have to get there indirectly with type erasure.
Swift has several type-erasers. AnySequence
, AnyGenerator
, AnyForwardIndex
, etc. These are generic versions of protocols. We can build our own AnyValidationRule
:
struct AnyValidationRule<InputType>: ValidationRule {
private let validator: (InputType) -> Bool
init<Base: ValidationRule where Base.InputType == InputType>(_ base: Base) {
validator = base.validate
}
func validate(input: InputType) -> Bool { return validator(input) }
}
The deep magic here is validator
. It's possible that there's some other way to do type erasure without a closure, but that's the best way I know. (I also hate the fact that Swift cannot handle validate
being a closure property. In Swift, property getters aren't proper methods. So you need the extra indirection layer of validator
.)
With that in place, you can make the kinds of arrays you wanted:
let len = ValidationRuleLength()
len.validate("stuff")
let cond = ValidationRuleCondition<String>()
cond.validate("otherstuff")
let rules = [AnyValidationRule(len), AnyValidationRule(cond)]
let passed = rules.reduce(true) { $0 && $1.validate("combined") }
Note that type erasure doesn't throw away type safety. It just "erases" a layer of implementation detail. AnyValidationRule<String>
is still different from AnyValidationRule<Int>
, so this will fail:
let len = ValidationRuleLength()
let condInt = ValidationRuleCondition<Int>()
let badRules = [AnyValidationRule(len), AnyValidationRule(condInt)]
// error: type of expression is ambiguous without more context