Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

}
like image 821
Adam Waite Avatar asked Aug 01 '15 12:08

Adam Waite


People also ask

Can associated types be used in protocol extensions?

A protocol can have one or more associated types and these associated types provide flexibility for conforming types to decide which type to use in place of each associated type placeholder. Associated types are specified with the associatedtype keyword.

What are protocols with associated types?

An associated type can be seen as a replacement of a specific type within a protocol definition. In other words: it's a placeholder name of a type to use until the protocol is adopted and the exact type is specified. This is best explained by a simple code example.

How do you ensure the adoption of a protocol only by reference type?

You can limit protocol adoption to class types (and not structures or enumerations) by adding the AnyObject or class protocol to a protocol's inheritance list.

Can protocols conform to protocols?

Protocols cannot conform to protocols. It may be easier to understand if existential protocol types were spelled differently from concrete types. In the last code snippet, when you write Set<Category> you are using Category as the existential type containing all the concrete types conforming to the Category protocol.

Can a struct conform to a protocol?

Classes, structs and enums can conform to multiple protocols and the conformance relationship can be established retroactively.

What is an associated type in Swift?

An associated type gives a placeholder name to a type that's used as part of the protocol. The actual type to use for that associated type isn't specified until the protocol is adopted. Associated types are specified with the associatedtype keyword.


1 Answers

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
like image 124
Rob Napier Avatar answered Sep 30 '22 20:09

Rob Napier