1

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

  • 2
    Just one question: How do you think should the `execute` function should behave if someone changed the order of `plugs.append` calls in `init` ? – Andreas Oetjen Apr 28 '20 at 10:37
  • Node would be inherited, and the execute would be overloaded. So one could create an AddNode, SubtractNode, DotProductNode, MagnitureNode...etc in each case the user would add plugs of the Type needed, then access the plugs how ever they need in the execute. The above was just to demonstrate the data types and what I would like to achieve. – Simon Anderson Apr 28 '20 at 21:05
  • 1
    My question - independently of how to _implement_ such a thing in swift - was more of: What is expected to happen if someone calls `plugs[0].value[1]` ? – Andreas Oetjen Apr 29 '20 at 05:09
  • In the example above it would fail as the plug would not be an array type. The developer when creating the array would know the order of the plugs. another reason for this design, is that you could have a node made up of nested nodes(container of nodes). This is all part of a backend, with a node based UI front end... if I can get over this hurdle. I might also be looking at it from the wrong direction, as I am drawing on my Python and C++ knowledge. – Simon Anderson Apr 29 '20 at 08:34
  • To me it sounds like using a bunch of _Any_ variables that will be cast by the developer to anything he/she thinks. This works well in languages like javascript or Objective C, even without any casting, but Swift does not you do do such a thing. So the solution could either be having a broad protocol with all the possible (accessor) methods, or using type checks all the way. – Andreas Oetjen Apr 29 '20 at 19:12
  • I managed to figure out a better way then using an Any, as I has read up a bit and alot of developers suggest only using Any as a last resort. To solve my problem I used an enum made up of different types. – Simon Anderson May 02 '20 at 22:31
  • When you say "it would fail," what do you expect the program to actually do? Should it generate a recoverable error, or do you mean crash? Or do you mean "the compiler should prevent it?" Or do you mean it's undefined behavior and literally anything can happen? Swift is very fond of "the compiler should prevent it," but since you appear to be trying to avoid that, you need to think about what you would actually want to happen in the case you're allowing. – Rob Napier May 04 '20 at 00:40
  • Moving to the enum puts you back into the "compiler will prevent it," which is going to work better with Swift. The moment you add "Any" you have to worry about all those corner cases that evaporate when you use a strong type. – Rob Napier May 04 '20 at 00:42
  • Hi Rob, what I meant by "fail" for the plug if it was an array is that it would do nothing. What I am trying to achieve is a way of storing a value in an object or class (Plug) and then have a list of Plugs, and if possible have one method that returns the data stored in the Plug as the data type it was when created. – Simon Anderson May 04 '20 at 08:04
  • Rob I enjoyed your talk on real world protocols – Simon Anderson May 04 '20 at 08:17

2 Answers2

0

I managed to find a solution that worked for my needs.

I am still looking at finding a better way to handle getting the data and their correct type.

import Foundation
import MetalKit

// Creating the PlugType Enum
enum PlugType{
    case Integer(Int?)
    case Float_(Float?)
    case Double_(Double?)
    case Vector3(simd_int3)

    // default types
    static func IntegerType() -> PlugType{ return PlugType.Integer(nil)}
    static func FloatType() -> PlugType{ return PlugType.Float_(nil)}
    static func DoubleType() -> PlugType{ return PlugType.Double_(nil)}
}

// Implements a way to retrieve the correct value type
extension PlugType{
    var IntegerValue: Int{
        switch self{
            case .Integer(let value):
                return value ?? 0
            default:
                return 0
        }
    }

    var FloatValue: Float{
        switch self
        {
        case .Float_(let value):
            return value ?? 0
        default:
            return 0
        }
    }

    var DoubleValue: Double{
        switch self
        {
        case .Double_(let value):
            return value ?? 0
        default:
            return 0
        }
    }
}

// Get the string representation of the PlugType
extension PlugType {
    var typeName: String{
        switch self {
        case .Integer: return "Integer"
        case .Float_: return "Float"
        case .Double_: return "Double"
        case .Vector3: return "Vector3"
        }
    }

    var swiftType: Any.Type {
        switch self {
        case .Integer: return Int.self
        case .Float_: return Float.self
        case .Double_: return Double.self
        case .Vector3: return simd_int3.self
        }
    }
}

class Plug{
    var _value:PlugType?
    var type:String? { get{ return _value?.typeName } }

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

    func geee<T>(_ input:T) -> T{
        switch type {
            case "Integer":
                return getVal(_value!.IntegerValue) as! T
            case "Double":
                return getVal(_value!.DoubleValue) as! T
            default:
            return 0 as! T
        }

    }

    func getVal(_ val:Int) -> Int {
        return val
    }
    func getVal(_ val:Float) -> Float {
        return val
    }
    func getVal(_ val:Double) -> Double {
        return val
    }
}

var plugs:[Plug] = []
var p1 = Plug(PlugType.Integer(2))
0

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
  • Awesome! Thank you Jessy, that works even better than the enumerator version I came up with. would it be possible to ad a method that one could call that would return the value as the type it stores. eg Plug.value value would return the variable stored in the plug as the type it is meant to be – Simon Anderson May 04 '20 at 08:02
  • 1
    Jessy I dig raywenderlich.com, been reading the swiftUI and Metal book. Lots of great info there – Simon Anderson May 04 '20 at 08:06
  • I think this code already does what you're asking. e.g. `Plug( SIMD3(repeating: 9) ).value.z` returns `9`. I see that you have optionals in the original code, but I can't tell if they're necessary. –  May 04 '20 at 12:20
  • Thank Jessy, you were correct. I was wondering in the case where I am storing an Array of doubles in a plug, is there some more functionality I will need to add( I see you added subscript). With the array of protocols, do you have to down cast them into there Plug when retrieving them every time? – Simon Anderson May 04 '20 at 22:18
  • var p1:AnyPlug = Plug([0.1, 1.1, 2.1]) var p2 = Plug([0.1, 1.1, 2.1]) p1.value //Fails as the protocol AnyPlug does not have value p1.anyValue //[0.1, 1.1, 2.1] p2.value //[0.1, 1.1, 2.1] p1.anyValue[0] // Fails - Value of type 'PlugValue' has no subscripts p2.value[0] – Simon Anderson May 04 '20 at 22:20
  • 1
    You are a freaking life saver, Thank you for all the help! – Simon Anderson May 05 '20 at 01:12
  • Jessy, what is the best way to cast the AnyPlug back to a Plug? reason I ask is I have some checks that are looking for name collisions, and I use the === to check if the object is not comparing against itself – Simon Anderson May 06 '20 at 02:05
  • I got the === working by constraining the AnyPlug protocol to class. Might there be a better way of implementing this? – Simon Anderson May 06 '20 at 09:17
  • I’m unclear on what you’re asking and there are a lot of comments now. Seems like a new question is in order. –  May 06 '20 at 09:29
  • Agreed, Im just chipping away at one wall after another XD – Simon Anderson May 06 '20 at 09:44
  • 1
    Hi Jessy, you were such a massive help with this ticket, I was wondering if you might have a moment to take a look at this one https://stackoverflow.com/questions/61675702/arithmetic-with-generics-and-protocols – Simon Anderson May 08 '20 at 09:09