0

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

Hudi Ilfeld
  • 1,905
  • 2
  • 16
  • 25
  • I would do this casting within the closure. It takes an `inout MyProtocol`, and mutates it however necessary. – Alexander May 08 '20 at 13:42
  • @Alexander-ReinstateMonica yeah, like i said this is my final resort. but this would force a casting each time i invoke the method. pretty tedious. also, why are you suggesting inout rather then a return value (more functional don't you think?) – Hudi Ilfeld May 08 '20 at 13:52
  • There's nothing functional about setting a new value in an existing array lol. If you're going for functional, `set(type:at:closure:)` would return a new `Values` instance altogether, with a new array containing the new value. – Alexander May 08 '20 at 13:57
  • I'll cook up an answer. – Alexander May 08 '20 at 13:57
  • 1
    What should happen in your code if the element at that index is not of type T? As you've written `set(at:closure:)`, I don't see how the caller could be implemented in general, since it is required to generate a T in all cases, which may not be possible. Can there be arbitrary other conforming types to MyProtocol, or is it really just "StructA or StructB?" – Rob Napier May 08 '20 at 13:59
  • Rob is asking good questions. Without further context, it's hard to say what should happen when such a cast fails. – Alexander May 08 '20 at 14:02
  • 1
    I think an example of the caller would be very useful here. Looking at this code, it's not clear what `set(at:closure:)` is buying the caller in any case. It feels like the caller has all the special knowledge and has to do all the error handling anyway, so the caller might as well just read the value from `arr`, manipulate it, and then assign it back into `arr` as it likes. – Rob Napier May 08 '20 at 14:04
  • 1
    @RobNapier I want the function to manipulate values of the array without being limited to the protocol properties, I could indeed cast the parameter in each closure but that is pretty tedious – Hudi Ilfeld May 08 '20 at 14:08
  • Understood; that's quite reasonable. – Rob Napier May 08 '20 at 14:26

2 Answers2

2

Use T.Type instead of MyProtocol.Type in the set(type:at:closure:) method.

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

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

Example:

var v = Values()
v.set(type: StructB.self, at: 1) {
    var value = $0
    value?.thirdProperty = 20
    return value
}

Do let me know if this is the right understanding of your requirement.

PGDev
  • 23,751
  • 6
  • 34
  • 88
1

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

Rob Napier
  • 286,113
  • 34
  • 456
  • 610
  • Thank you for your thorough elaboration on PGDev's solution. Your time is much appreciated. My question to you is: what exactly does `T.Type = T.self` mean and what exactly are you gaining with it over just T.Type? TIA – Hudi Ilfeld May 08 '20 at 14:50
  • Adding `T.Type = T.self` creates a default parameter so you don't have to include it when the type is known from context, but you can provide it in cases where the type is ambiguous. – Rob Napier May 08 '20 at 14:53