8

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 ValidationRules:

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 rule2s 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]) {

}
Adam Waite
  • 19,175
  • 22
  • 126
  • 148

1 Answers1

14

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
Rob Napier
  • 286,113
  • 34
  • 456
  • 610
  • That's a great answer, thanks. I don't think I have ever seen anything like this before. Where did you learn that? Interested in reading some more. – Adam Waite Aug 03 '15 at 08:36
  • 4
    My favorite source is the Swift header file itself. There's a ton of documentation in there, including quite a few "why"s if you read it carefully. And I spend a lot of time trying to build weird data structures and complaining about it on Twitter when they don't work and having @jckarter correct me. http://airspeedvelocity.net is also a very good source for the topics that interest me. – Rob Napier Aug 03 '15 at 11:19
  • How come you need the closure and can't just store "base" and call validate on the stored base when you want to call validate? – JPC Aug 25 '16 at 19:39
  • 1
    @JPC the best way to understand that is to try implementing it. You'll find the wall almost instantly. – Rob Napier Aug 26 '16 at 02:04