swiftgenericsheterogeneous-array

Storing Generic Objects in a Heterogeneous Array and retrieving object parameter as the correct type


Ahoy everyone,

I have recently been trying to implement a Node based graph system that passes data between nodes using plugs. Similar to many of the 3D applications like houdini and maya.

I have written a similar system before using Python, and wanted to try this with Swift as my first learning excersise. Boy did I jump into the deep end on this one.

I am stuck now with Swifts Arrays, as I would like to store a list of Generic plugs. Each plug can have its own value type float, int, color, string, Vector Matrix.

I have read up about Type Erasers and opaque types, but still cant seem to get my values our of a list in a way that I can perform some arithmetic on them.

All and any help that might put me in the direction would be greatly appreciated :D

import Foundation
import MetalKit

protocol genericPlug {
    associatedtype T
    func GetValue() -> T
}


class Plug<T>:genericPlug{
    var _value:T?
    var value:T {
        get{GetValue()}
        set(val){
            value = val
        }
    }

    func GetValue() -> T{
        return _value!
    }

    init(_ newValue:T){
        _value=newValue
    }
}

class Node{
    var plugs:[genericPlug] = []
    init(){
        var p1 = Plug<Int>(0)
        var p2 = Plug(vector2(1.2, 3.1))
        var p3 = Plug([0.0, 3.1, 0.6, 1])

        plugs.append(p1)
        plugs.append(p2)
        plugs.append(p3)
    }

    func execute(){
        // will access the plugs in the array and perform some sort of calculations on them.
        plugs[0].value + 1      // should equal 1
        plugs[1].value.x + 0.8  // should have x=2.0 y=3.1
        plugs[2].value[1] - 0.1 // should equal 3.0
    }
}

Thanks everyone


Solution

  • Use a generic something to extract what you need. Your options are methods and subscripts.

    protocol PlugValue {
      init()
    }
    
    extension Int: PlugValue { }
    extension Float: PlugValue { }
    extension Double: PlugValue { }
    extension SIMD3: PlugValue where Scalar == Int32 { }
    
    struct Plug<Value: PlugValue> {
      var value: Value
    
      init(_ value: Value) {
        self.value = value
      }
    }
    
    protocol AnyPlug {
      var anyValue: PlugValue { get }
    }
    
    extension AnyPlug {
      subscript<Value: PlugValue>(type: Value.Type = Value.self) -> Value {
        anyValue as? Value ?? .init()
      }
    
      func callAsFunction<Value: PlugValue>(_ type: Value.Type = Value.self) -> Value {
        anyValue as? Value ?? .init()
      }
    }
    
    extension Plug: AnyPlug {
      var anyValue: PlugValue { value }
    }
    
    let plugs: [AnyPlug] = [
      Plug(1),
      Plug(2.3 as Float),
      Plug(4.5),
      Plug([6, 7, 8] as SIMD3)
    ]
    
    plugs[0][Int.self] // 1
    plugs[1][Double.self] // 0
    plugs[1][] as Float // 2.3
    let double: Double = plugs[2]() // 4.5
    plugs[3](SIMD3.self).y // 7
    

    With the array of protocols, do you have to down cast them into their Plug when retrieving them every time?

    Essentially. This is true of all heterogenous sequences. Here are your options:

    extension Array: PlugValue where Element: PlugValue { }
    
    let plug: AnyPlug = Plug([0.1, 1.1, 2.1])
    (plug as? Plug<[Double]>)?.value[1]
    (plug.anyValue as? [Double])?[1]
    
    extension Plug {
      enum Error: Swift.Error {
        case typeMismatch
      }
    }
    
    extension AnyPlug {
      func callAsFunction<Value: PlugValue, Return>(_ closure: (Value) -> Return) throws {
        guard let value = anyValue as? Value
        else { throw Plug<Value>.Error.typeMismatch }
    
        closure(value)
      }
    }
    
    try plug { (doubles: [Double]) in doubles[1] } // 1.1
    try plug { ($0 as [Double])[1] } // 1.1
    try plug { $0 as Int } // <Swift.Int>.Error.typeMismatch