iosswiftgenericsheterogeneous-array

How To constraint parameter of generic type in function, to sibling parameter


To understand the origin of the question, let's start with some code:

protocol MyProtocol {
   var val1: Int { get set }
}


struct StructA: MyProtocol {
   var val1: Int
   var structAVal: Int
}


struct StructB: MyProtocol {
   var val1: Int
   var structBVal: Int 
   var thirdProperty: Int
}

And then I have a struct with a heterogeneous array of type MyProtocol:

struct Values {
    var arr: [MyProtocol] = [StructA(val1: 0, structAVal: 0), StructB(val1: 0, structBVal: 0)]
}

if I was to change one of the values with a method in Values such as:

  struct Values {
    var arr: [MyProtocol] = [StructA(val1: 0, structAVal: 0), StructB(val1: 0, structBVal: 0)]

    mutating func set<T: MyProtocol>(at index: Int, _ newValue: T) {
        arr[index] = newValue
    }
}

That would be smooth. The problem which I am facing is, say I wanted to change var thirdProperty: Int in the structB item in var arr: [MyProtocol], I would not be able to do so which my mutating func set<T: MyProtocol>(at index: Int, _ newValue: T), since It only knows of MyProtocol types.

So my 2 cents to resolve this matter was using a closure something like this:

 mutating func set<T: MyProtocol>(at index: Int, closure: (T?) -> (T)) {
        arr[index] = closure(arr[index] as? T)
 }

The problem with this is that every time I invoke this method, I would first need to downcast the parameter (from MyProtocol to StructB). which seems more of a workaround which could invite unwanted behaviours along the road.

So I started thinking maybe there is a way to constraint the generic parameter to a sibling parameter something like this (pseudo code):

 mutating func set<T: MyProtocol>(type: MyProtocol.Type, at index: Int, closure: (T?) -> (T)) where T == type {
        arr[index] = closure(arr[index] as? T)
}

Which as you guessed, does not compile.

Any thought on how to approach this matter in a better manner. T.I.A


Solution

  • PGDev's solution gets to the heart of the question, but IMO the following is a bit easier to use:

    enum Error: Swift.Error { case unexpectedType }
    mutating func set<T: MyProtocol>(type: T.Type = T.self, at index: Int,
                                         applying: ((inout T) throws -> Void)) throws {
        guard var value = arr[index] as? T else { throw Error.unexpectedType }
        try applying(&value)
        arr[index] = value
    }
    
    ...
    
    var v = Values()
    try v.set(type: StructB.self, at: 1) {
        $0.thirdProperty = 20
    }
    

    The = T.self syntax allows this to be simplified a little when the type is known:

    func updateThirdProperty(v: inout StructB) {
        v.thirdProperty = 20
    }
    try v.set(at: 1, applying: updateThirdProperty)
    

    Another approach that is more flexible, but slightly harder on the caller, would be a closure that returns MyProtocol, so the updating function can modify the type. I'd only add this if it were actually useful in your program:

    mutating func set<T: MyProtocol>(type: T.Type = T.self, at index: Int,
                                     applying: ((T) throws -> MyProtocol)) throws {
        guard let value = arr[index] as? T else { throw Error.unexpectedType }
        arr[index] = try applying(value)
    }
    
    ...
    
    try v.set(type: StructB.self, at: 1) {
        var value = $0
        value.thirdProperty = 20
        return value // This could return a StructA, or any other MyProtocol
    }
    

    (Which is very close to PGDev's example, but doesn't require Optionals.)