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).
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)