Home > Net >  Generic constraint
Generic constraint

Time:09-22

I'm trying to implement a generic validator in Swift. The validator simply performs some validation, i.e. email validation, and returns true or false based on whether or not the value is valid.

protocol Validator {
    associatedtype Value
    
    func validate(_ value: Value) -> Bool
}

I've also managed to get these validators implemented. As you can see, the associatedValue works like a charm.

struct EmailValidator: Validator {
    func validate(_ value: String) -> Bool {
        NSPredicate(format: "SELF MATCHES %@", "[A-Z0-9a-z._% -] @[A-Za-z0-9.-] \\.[A-Za-z]{2,}").evaluate(with: value)
    }
}

struct RequiredValidator: Validator {
    func validate(_ value: String) -> Bool {
        !value.isEmpty
    }
}

But now I'd like to have a compound validator, that doesn't have to conform Validator, but it does need to accept any Validator where the associatedValue is the same. Below I have an example.

let compoundValidator = CompoundValidator<String>(EmailValidator(), RequiredValidator())

However, when I try to implement this idea, I run into errors such as Cannot specialize non-generic type 'Validator' or Cannot convert value of type 'Value' to expected argument type 'Validator.Value' or Protocol 'Validator' can only be used as a generic constraint because it has Self or associated type requirements depending on what I try to implement. Is there a way I can achieve this compound validator using generics?

struct CompoundValidator<Value> {
    let validators: [Validator<Value>]
    
    init(_ validators: Validator<Value>...) {
        self.validators = validators
    }
    
    func validate(_ value: Value) -> Bool {
        validators.reduce(into: true) { partialResult, validator in
            partialResult = partialResult && validator.validate(value)
        }
    }
}

CodePudding user response:

Let's start with the beginning, Validator<Value> is invalid syntax, even if Validator is a protocol with associated types. Swift doesn't yet allow existential containers (protocol references) to declare generic constraints, the exception being the where clause.

Thus, you'll need a type eraser to be able to use any kind of generics:

struct AnyValidator<Value>: Validator {
    private let _validate: (Value) -> Bool
    
    init<V: Validator>(_ validator: V) where V.Value == Value {
        _validate = validator.validate
    }
    
    func validate(_ value: Value) -> Bool {
        _validate(value)
    }
}

extension Validator {
    func eraseToAnyValidator() -> AnyValidator<Value> {
        AnyValidator(self)
    }
}

, and you'll need to update the definition as such:

struct CompoundValidator<Value> {
    let validators: [AnyValidator<Value>]
    
    init(_ validators: AnyValidator<Value>...) {
        self.validators = validators
    }

However the above implementation requires all callers to call .eraseToAnyValidator():

let compoundValidator = CompoundValidator(EmailValidator().eraseToAnyValidator(), RequiredValidator().eraseToAnyValidator())

If you want to decrease the burden on the caller, then you'll need do some work on the callee side:

struct CompoundValidator<Value> {
    let validators: [AnyValidator<Value>]
       
    init<V: Validator>(_ v: V) where V.Value == Value {
        validators = [v.eraseToAnyValidator()]
    }
    
    init<V1: Validator, V2: Validator>(_ v1: V1, _ v2: V2) where V1.Value == Value, V2.Value == Value {
        validators = [v1.eraseToAnyValidator(), v2.eraseToAnyValidator()]
    }
    
    init<V1: Validator, V2: Validator, V3: Validator>(_ v1: V1, _ v2: V2, _ v3: V3) where V1.Value == Value, V2.Value == Value, V3.Value == Value {
        validators = [v1.eraseToAnyValidator(), v2.eraseToAnyValidator(), v3.eraseToAnyValidator()]
    }

, basically you'll need to add as many initializers as many arguments you'll want to pass through the codebase. This enables you to call the initializer in the simple form:

let compoundValidator = CompoundValidator(EmailValidator(), RequiredValidator())

CodePudding user response:

struct CompoundValidator<Value, T: Validator> where T.Value == Value {
    let validators: [T]

    init(_ validators: T...) {
        self.validators = validators
    }

    func validate(_ value: Value) -> Bool {
        validators.reduce(into: true) { partialResult, validator in
            partialResult = partialResult && validator.validate(value)
        }
    }
}

CodePudding user response:

You can create an AnyValidator type, that is a concrete type. This way you can use it to specify the generic type arguments.

struct AnyValidator<Value>: Validator {
    private let validateFunc: (Value) -> Bool
    
    init<T: Validator>(_ validator: T) where T.Value == Value {
        validateFunc = validator.validate
    }
    
    func validate(_ value: Value) -> Bool {
        validateFunc(value)
    }
}

extension Validator {
    func eraseToAnyValidator() -> AnyValidator<Value> {
        .init(self)
    }
}

struct CompoundValidator<Value> {
    let validators: [AnyValidator<Value>]
    
    init(_ validators: AnyValidator<Value>...) {
        self.validators = validators
    }
    
    func validate(_ value: Value) -> Bool {
        validators.reduce(into: true) { partialResult, validator in
            partialResult = partialResult && validator.validate(value)
        }
    }
}

Example usage:

CompoundValidator(
    EmailValidator().eraseToAnyValidator(),
    RequiredValidator().eraseToAnyValidator())

You might want to provide convenience initialisers for few numbers of validators:

init<V1, V2>(_ v1: V1, _ v2: V2) where V1: Validator, V2: Validator, V1.Value == Value, V2.Value == Value {
    self.init(v1.eraseToAnyValidator(), v2.eraseToAnyValidator())
}
init<V1, V2, V3>(_ v1: V1, _ v2: V2, _ v3: V3) where V1: Validator, V2: Validator, V3: Validator, V1.Value == Value, V2.Value == Value, V3.Value == Value {
    self.init(v1.eraseToAnyValidator(), v2.eraseToAnyValidator(), v3.eraseToAnyValidator())
}

which allows you to omit .eraseToAnyValidator().

This is similar to how Combine's Publisher type has a merge operator that accepts up to eight arguments, just to allow you to merge different types of Publishers without saying eraseToAnyPublisher.

  • Related