简体   繁体   中英

In Swift, how can I implement a generic system that processes a type by registering processors for different key paths?

I want to implement a system that allows me to generically process a type by registering processors for different key paths.

One trait of the system should be composition , so every processor should extend one common generic protocol Processor .

Example of usage:

struct Language {
    var name = "Swift"
    var version = 5.3
}

var processor = TypeProcessor<Language>()
processor.add(procesor: VersionProcessor(), keypath: \.version)
processor.add(procesor: NameProcessor(), keypath: \.name)

var input = Language()
processor.process(value: input)

// Languge version:     5.3
// Languge name:        Swift

Instead of trying to erase property types, your can use inheritance of "processors", where subclasses are generic over root object and property types and have property key path.

protocol Processor {
    associatedtype T
    func process(value: T)
}

class AnyProcessor<Value>: Processor {
    func process(value: Value) {
    }
}

final class KeyPathProcessor<Root, P: Processor>: AnyProcessor<Root> {
    typealias Value = P.T

    let keyPath: KeyPath<Root, Value>
    let processor: P

    init(keyPath: KeyPath<Root, Value>, processor: P) {
        self.keyPath = keyPath
        self.processor = processor
    }

    override func process(value: Root) {
        processor.process(value: value[keyPath: keyPath])
    }
}

struct ObjectProcessor<T>: Processor {
    var processors = [AnyKeyPath: AnyProcessor<T>]()

    mutating func add<P: Processor, V>(processor: P, keypath: KeyPath<T, V>) where P.T == V {
        self.processors[keypath] = KeyPathProcessor(keyPath: keypath, processor: processor)
    }

    func process(value: T) {
        for (_, processor) in processors {
            processor.process(value: value)
        }
    }
}

I've created a playground showing how this can be solved with function composition and then using what we've learned there to recreate your example.

Function composition allows you to create new functions by chaining together existing ones, as long as the types match up.

precedencegroup CompositionPrecedence {
    associativity: left
}

infix operator >>>: CompositionPrecedence

func >>> <T, U, V>(lhs: @escaping (T) -> U, rhs: @escaping (U) -> V) -> (T) -> V {
    return { rhs(lhs($0)) }
}

The Processor can be translated to a function that takes an object O , transforms it in some way, and returns a new object. Creating the function could be done like this:

func process<O, K>(keyPath: WritableKeyPath<O, K>, _ f: @escaping (K) -> K) -> (O) -> O {
    return { object in
        var writable = object
        writable[keyPath: keyPath] = f(object[keyPath: keyPath])
        return writable
    }
}

let reverseName = process(keyPath: \Person.name, reverse)
let halfAge = process(keyPath: \Person.age, half)

Now we can compose those two functions. The resulting functions still keeps the signature `(Person) -> Person). We can compose as many functions as we like, creating a processing pipeline.

let benjaminButton = reverseName >>> halfAge
let youngBradPitt = benjaminButton(bradPitt)

Moving on to recreating your example. As this answer mentions, the type is generic over the root object. This is just like in the function composition example, and it allows us to group all the processors in an array for example.

protocol Processor {
    associatedtype T
    func process(object: T) -> T
}

When erasing an object, it's important to keep a reference to the original, so that we can use it to implement the required functionality. In this case, we're keeping a reference to its process(:) method.

extension Processor {
    func erased()-> AnyProcessor<T> {
        AnyProcessor(base: self)
    }
}

struct AnyProcessor<T>: Processor {
    private var _process: (T) -> T
    
    init<Base: Processor>(base: Base) where Base.T == T {
        _process = base.process
    }
    
    func process(object: T) -> T {
        _process(object)
    }
}

Here's two types implementing the Processor protocol. Notice the first one has two placeholder types. The second placeholder will get erased.

struct AgeMultiplier<T, K: Numeric>: Processor {
    let multiplier: K
    let keyPath: WritableKeyPath<T, K>
    
    private func f(_ value: K) -> K {
        value * multiplier
    }
    
    func process(object: T) -> T {
        var writable = object
        writable[keyPath: keyPath] = f(object[keyPath: keyPath])
        return writable
    }
}

struct NameUppercaser<T>: Processor {
    let keyPath: WritableKeyPath<T, String>
    
    private func f(_ value: String) -> String {
        value.uppercased()
    }
    
    func process(object: T) -> T {
        var writable = object
        writable[keyPath: keyPath] = f(object[keyPath: keyPath])
        return writable
    }
}

Finally, the ObjectProcessor that uses object composition. Notice the array holds object of the same type. An instance of this struct will only be able to process Person s for example. What each child processor does is hidden away in the implementation and the fact it may run on different kinds of data does not affect the ObjectProcessor .

struct ObjectProcessor<T>: Processor {
    private var processers = [AnyProcessor<T>]()
    
    mutating func add(processor: AnyProcessor<T>) {
        processers.append(processor)
    }
    
    func process(object: T) -> T {
        var object = object
        
        for processor in processers {
            object = processor.process(object: object)
        }
        
        return object
    }
}

And here it is in action. Notice I add two processors for the same key.

var holyGrail = ObjectProcessor<Person>()
holyGrail.add(processor: NameUppercaser(keyPath: \Person.name).erased())
holyGrail.add(processor: AgeMultiplier(multiplier: 2, keyPath: \Person.age).erased())
holyGrail.add(processor: AgeMultiplier(multiplier: 3, keyPath: \Person.age).erased())

let bradPitt = Person(name: "Brad Pitt", age: 57)
let immortalBradPitt = holyGrail.process(object: bradPitt)

Thank you for your answers. I've used your inputs and come up with the following solution:

protocol Processor {
    
    associatedtype T
    
    func process(value: T)
}

extension Processor {
    
    func erase()-> AnyProcessor<T> {
        return AnyProcessor<T>(self)
    }
}


struct AnyProcessor<T>: Processor {
    
    let process: (T) -> Void
    
    init<P: Processor>(_ processor: P) where P.T == T {
        self.process = processor.process
    }
    
    func process(value: T) {
        self.process(value)
    }
}

struct KeyPathProcessor<T, V>: Processor {
    
    private var keyPath: KeyPath<T,V>
    private var processor: AnyProcessor<V>
    
    init<P: Processor>(_ processor: P, for keyPath: KeyPath<T, V>) where P.T == V {
        self.processor = processor.erase()
        self.keyPath = keyPath
    }
    
    func process (value: T) {
        let input = value[keyPath: keyPath]
        processor.process(value: input)
    }
}

struct VersionProcessor: Processor {
    
    func process(value: Double) {
        print("Languge version:     \(value)")
    }
}

struct NameProcessor: Processor {
    
    func process(value: String) {
        print("Languge name:        \(value)")
    }
}

struct TypeProcessor<T>: Processor {
    
    var processors = [AnyProcessor<T>]()
    
    mutating func add<P: Processor, V>(procesor: P, keypath: KeyPath<T, V>) where P.T == V {
        let p = KeyPathProcessor(procesor, for: keypath).erase()
        self.processors.append(p)
    }
    
    func process(value: T) {
        for processor in processors {
            processor.process(value: value)
        }
    }
}

struct Language {
    var name = "Swift"
    var version = 5.3
}

var processor = TypeProcessor<Language>()
processor.add(procesor: VersionProcessor(), keypath: \.version)
processor.add(procesor: NameProcessor(), keypath: \.name)

var input = Language()
processor.process(value: input)

// Languge version:     5.3
// Languge name:        Swift

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM