2

Seeing strange generic behavior, which leads me to believe I'm missing something in my understanding.

I am using the following method to loop throw a JSON response and call a generic method. User, Card and Ecard all inherit from IDObject, which in turn inherits from Object (a Realm class)

let props:[(label:String, type:IDObject.Type)] = [
    (label: "deletedUsers", type: User.self),
    (label: "deletedCards", type: Card.self),
    (label: "deletedECards", type: Ecard.self)
]

for prop in props {
    if let ids = json[prop.label].arrayObject as? [Int], ids.count > 0 {
        DataManager.shared.delete(prop.type, ids: ids)
    }
}

func delete<T:IDObject>(_ type:T.Type, ids:[Int]) {
    guard ids.count > 0 else { return }
    if let objectsToDelete = objects(type, where: NSPredicate(format: "identifier IN %@", ids)) {
        delete(objectsToDelete)
    }
}

func delete<T:Object>(_ objects:Results<T>) {
    guard objects.count > 0 else { return }
    do {
        let realm = try Realm()
        try realm.write {
            realm.delete(objects)
        }
    }  catch {
        print(error)
    }
}

The delete(_ type:T.Type, ids:[Int]) function can not infer the generic type this way.

However, unwrapping the for prop in props loop works as expected.

if let userIds = json["deletedUsers"].arrayObject as? [Int], userIds.count > 0 {
    DataManager.shared.delete(User.self, ids: userIds)
}

Do generics only work at compile time, or is there a way to handle this dynamically at runtime?

dmorrow
  • 5,152
  • 5
  • 20
  • 31
  • What exactly do you mean by "*The `delete` function can not infer the generic type this way*"? You get a compiler error? (if so, where?) You're missing a closing parenthesis off `DataManager.shared.delete(prop.type, ids: ids`, and unless you've overload `delete` with a single parameter, you cannot call it with a single parameter (`delete(objectsToDelete)`). Please could you provide a [mcve], including the expected behaviour and actual behaviour? – Hamish Feb 17 '17 at 18:05
  • @Hamish The entire code base is very large, and I'm not at liberty to share it. I've heavily edited it here, and I've fixed the missing closing parenthesis. `delete` is indeed overloaded - the `delete(:)` method expects a `Results` - `IDObject` inherits from `Object`. The behavior I'm seeing is in the delete(objects:) method, objects.count == 0 – dmorrow Feb 17 '17 at 18:38
  • You don't need to share the entire code base – only a minimal self-contained example which reproduces the same problem. Is `DataManager.shared.delete` supposed to refer to `delete(_:ids:)`? What does `objects(_:where:)` return? (You don't even have to show the function, just mock a function which reproduces the same issue). Is `ids` even relevant to the problem? (if not, remove it). – Hamish Feb 17 '17 at 18:51

1 Answers1

2

Generics are evaluated at compile time and assigned a single, concrete type. There is no such thing as "type inference at runtime."

I think the primary change you want is:

func delete(_ type:IDObject.Type, ids:[Int]) {

You don't want to specialize this function on type, you just want to pass type.

It's not clear what objects(_:where:) returns, so this may break your delete method. You may need to make it less specific:

func delete(_ objects:Results<Object>) {

(This isn't a panacea for subtyping; I'm assuming that objects(_:where:) returns exactly Results<Object>.)

Rob Napier
  • 286,113
  • 34
  • 456
  • 610
  • I *think* by "type inference at runtime" he means something like [this](http://stackoverflow.com/a/30945263/5175709) read **Protocols can choose static or dynamic dispatch.** he basically means static vs dynamic dispatch. – mfaani Feb 17 '17 at 18:55
  • I might be missing something here, but I don't see how changing the static type of the `type` parameter will change the runtime behaviour of OP's code (considering that when he's calling `delete` (assuming it's actually `DataManager.shared.delete`), `T` is already being inferred to be `IDObject`). Also note that generics are only specialised as a compiler optimisation. – Hamish Feb 17 '17 at 19:51
  • It may already be inferred as `IDObject`, but specializing on it is at best confusing since the reader is likely to believe more is happening (it's hard to test because this code won't compile). "Specialization" doesn't have to mean that a unique version of the function is written by the compiler. `Array` is a specialization of `Array` over `Int`, regardless of how it's implemented by the compiler. – Rob Napier Feb 17 '17 at 20:34
  • @RobNapier Okay, you're right w.r.t "specialisation" :) The only point I was trying to make is that "*Generics are evaluated at compile time and assigned a single, concrete type*" is slightly misleading as it, to me at least, implies that the compiler always produces specialised versions of generic functions. (also perhaps worth noting that unconstrained generic placeholders don't have to be satisfied by concrete types) – Hamish Feb 17 '17 at 21:08
  • @RobNapier - thank you for this explanation. What I was trying to achieve was to loop through a set of data and map it (in Realm) to different data models, which are all subclasses of `IDObject`. However, as I discovered, the compiler was using `IDObject` as the generic ``, not the subclasses. I originally tried to just pass `type`, as you suggest, but also did not get the results I was looking for. I may need to revisit this approach again. This may also be an issue with how the Realm library does its mapping. – dmorrow Feb 27 '17 at 15:09