swiftgenericsgraphprotocols

Swift Execution Graph: Protocol + Generics Impasse


I'm trying to create a generic acyclic execution graph composed of nodes. Each node has 0 or more inputs and outputs. Each input and output is typed for a specific object type (Double, UIImage, String, etc.). I want to only be able to connect inputs and outputs with matching object types in a 1-to-1 relationship (no multiplexing). It all works right up to the point where I tried to create the Connection object which represents the existence of a connection between a specific input and output on different nodes. Here are simplified versions of my types:

import UIKit

protocol NodeOutput {
    associatedtype Entity
    
    var type: Entity.Type { get }
    var value: Entity { get }
}

protocol NodeInput {
    associatedtype Entity
        
    var type: Entity.Type { get }
    func set(value: Entity)
}

protocol Node {
    var inputs: [any NodeInput] { get }
    var outputs: [any NodeOutput] { get }
}

struct GenericConnection<Input: NodeInput, Output: NodeOutput> where Input.Entity == Output.Entity {
    
    public let input: Input
    public let output: Output
    
    /// Create a new connection object.
    public init(input: Input, output: Output) throws {
        self.input = input
        self.output = output
        
        input.set(value: output.value)
    }
    
    public func activate() throws {
        input.set(value: output.value)
    }
}

// Actual Implementations

class SimpleNodeInput<Entity>: NodeInput {
    
    public var type: Entity.Type { Entity.self }
    public private(set) var value: Entity
    
    public init(value: Entity) {
        self.value = value
    }
    
    public func set(value: Entity) {
        self.value = value
    }
}

class SimpleNodeOutput<Entity>: NodeOutput {
    public var type: Entity.Type { Entity.self }
    public var value: Entity
    
    public init(value: Entity) {
        self.value = value
    }
}

class ConstantColorNode: Node {
    let inputs: [any NodeInput] = []
    private(set) var outputs: [any NodeOutput] = []
    
    public private(set) var color: UIColor {
        didSet {
            _colorOutput.value = color
        }
    }
    
    private var _colorOutput: SimpleNodeOutput<UIColor>
    var colorOutput: any NodeOutput { _colorOutput as any NodeOutput }
    
    public init(color: UIColor) {
        self.color = color
        _colorOutput = SimpleNodeOutput(value: color)
        self.outputs = [colorOutput]
    }
    
    func set(color: UIColor) {
        self.color = color
    }
}

class FinalOutputNode<T>: Node {
    private(set) var inputs: [any NodeInput] = []
    let outputs: [any NodeOutput] = []
    
    var finalInput: any NodeInput
    
    init(initialValue: T) {
        
        finalInput = SimpleNodeInput(value: initialValue)
        self.inputs = [finalInput]
    }
}

let colorNode = ConstantColorNode(color: .red)
let outputNode = FinalOutputNode<UIColor>(initialValue: .blue)
let genericConnection = try GenericConnection(input: outputNode.finalInput, output: colorNode.colorOutput)

So... the Connection type has a compiler error that set cannot be used on type any NodeInput. The GenericConnection type does not have a compiler error until you try to instantiate it, then you get the error that any NodeInput does not conform to NodeInput and any NodeOutput does not conform to NodeOutput.

I'm really not sure how I can model this connection between nodes that need to be constrained to having the same generic type. For the record, I have multiple input and output node types that do things like emit a constant value or query a delegate for their type and value. Just not sure how to do this right, or if its even expressible elegantly without making all of the data Any and then doing runtime type checking (which seems super-gross).


Solution

  • You could use primary associated types :

    protocol NodeOutput<Entity> {
        associatedtype Entity
        
        var type: Entity.Type { get }
        var value: Entity { get }
    }
    
    protocol NodeInput<Entity> {
        associatedtype Entity
        
        var type: Entity.Type { get }
        func set(value: Entity)
    }
    
    protocol Node<Entity> {
        associatedtype Entity
        var inputs: [any NodeInput<Entity>] { get }
        var outputs: [any NodeOutput<Entity>] { get }
    }
    
    struct GenericConnection<T> {
        
        public let input: any NodeInput<T>
        public let output: any NodeOutput<T>
        
            /// Create a new connection object.
        public init(input: any NodeInput<T>, output: any NodeOutput<T>) throws {
            self.input = input
            self.output = output
            
            input.set(value: output.value)
        }
        
        public func activate() throws {
            input.set(value: output.value)
        }
    }
    
        // Actual Implementations
    
    class SimpleNodeInput<Entity>: NodeInput {
        
        public var type: Entity.Type { Entity.self }
        public private(set) var value: Entity
        
        public init(value: Entity) {
            self.value = value
        }
        
        public func set(value: Entity) {
            self.value = value
        }
    }
    
    class SimpleNodeOutput<Entity>: NodeOutput {
        public var type: Entity.Type { Entity.self }
        public var value: Entity
        
        public init(value: Entity) {
            self.value = value
        }
    }
    
    class ConstantColorNode: Node {
        let inputs: [any NodeInput<UIColor>] = []
        private(set) var outputs: [any NodeOutput<UIColor>] = []
        
        public private(set) var color: UIColor {
            didSet {
                _colorOutput.value = color
            }
        }
        
        private var _colorOutput: SimpleNodeOutput<UIColor>
        var colorOutput: any NodeOutput<UIColor> { _colorOutput as any NodeOutput<UIColor> }
        
        public init(color: UIColor) {
            self.color = color
            _colorOutput = SimpleNodeOutput(value: color)
            self.outputs = [colorOutput]
        }
        
        func set(color: UIColor) {
            self.color = color
        }
    }
    
    class FinalOutputNode<T>: Node {
        private(set) var inputs: [any NodeInput<T>] = []
        let outputs: [any NodeOutput<T>] = []
        
        var finalInput: any NodeInput<T>
        
        init(initialValue: T) {
            
            finalInput = SimpleNodeInput(value: initialValue)
            self.inputs = [finalInput]
        }
    }
    
    struct ConnectionBundle {
        public let input: any NodeInput
        public let output: any NodeOutput
        
        init(input: any NodeInput, output: any NodeOutput) {
            self.input = input
            self.output = output
        }
    }
    
    
    let colorNode = ConstantColorNode(color: .red)
    let outputNode = FinalOutputNode<UIColor>(initialValue: .blue)
    let genericConnection = try GenericConnection(input: outputNode.finalInput, output: colorNode.colorOutput)