Generic cast method

I'm not sure why this is not possible, can someone provide some insight on how to get around it or what the dealio is? :slight_smile:

What I'm trying to do is have a base class with a generic parameter. Then I'd subclass it pass those to the printer. The printer would try to cast, if successful - it'd call show and the right method would get invoked, based on which subclass it is.

class ContainerPrinter

{

// This gives - Cannot specialize non-generic type 'Container'
func cast<Container, Element>(container: Container<Element>) -> Container<P>? {
    if Element.self == P.self {
        return (container as! Container<P>)
    } else {
        return nil
    }
}

func show(container: DogContainer<P>) {
    print("dog")
}

func show(container: CatContainer<P>) {
    print("cat")
}

func show(container: BaseContainer<P>) {
    print("base")
}

// This works but I'd need to switch on every subclass, yikes
func show<M>(container: BaseContainer<M>) {
    print("regular")
    if let cat = container as? CatContainer<P> {
        show(container: cat)
    }
}

}

class BaseContainer {

}

class CatContainer: BaseContainer {

}

class DogContainer: BaseContainer {

}

I know this isn't exactly what you described, but does this work?

class Container<Element> {
    let element: Element
    
    init(_ element: Element) {
        self.element = element
    }
}

struct Cat {
    let name: String
}

struct Dog {
    let name: String
}


extension Container where Element == Cat {
    func show() {
        print("cat")
    }
}

extension Container where Element == Dog {
    func show() {
        print("dog")
    }
}


class CatContainer: Container<Cat> { }
let cat = Cat(name: "Felix")
let catContainer = CatContainer(cat)

catContainer.show()

If not, can you talk a little bit more about what you're trying to achieve?

Hi twof,

It works, if there is ever one way to show a container. (one printer class in my original example)

Let me try a real world example, I think my over-simplified toy example may be insufficient to illustrate what I'm really wanting to achieve.

What I'd like is to be able to add/remove handleEvent methods on various classes, without needing to add them to a switch statement in genericHandler, to handle the various subclasses that'll appear in the future, as more different types of events get added down the line.

The difficulty is in that pesky generic parameter on Event. I'd like to keep it if at all possible.

class CollectionModel<Model> {

// This gives - Cannot specialize non-generic type 'Container'
func cast<Cont, Element>(event: Cont<Element>) -> Cont<P>? {
    if Element.self == Model.self {
        return (event as! Cont<P>)
    } else {
        return nil
    }
}

func handleEvent(event: AddEvent<Model>) {
    print("add to collection")
}

func handleEvent(event: RemoveEvent<Model>) {
    print("remove from collection")
}
func handleEvent(event: BaseEvent<Model>) {
    print("ignore base event")
}
// This works but I'd need to switch on every subclass, yikes
// What I'd like is for this switch to be handled generically, via cast
func genericHandler<M>(event: BaseEvent<M>) {
    switch event {
    case let removeEvent as RemoveEvent<Model>:
        handleEvent(event: removeEvent)
    case let addEvent as AddEvent<Model>:
        handleEvent(event: addEvent)
    default:
        print("ignore - no handler")
    }

    // Desired way to accomplish this
    if let castEvent = cast(event) {
        handleEvent(event: castEvent)
    }
}

}

class BaseEvent<M> {

}

class AddEvent<M>: BaseEvent<M> {

}

class RemoveEvent<M>: BaseEvent<M> {

}

let collection = CollectionModel<NSObject>()

let addEvent = AddEvent<NSObject>()
let secretAddEvent: BaseEvent<NSObject> = AddEvent<NSObject>()

collection.genericHandler(event: addEvent)

Thanks for clarifying further!

I think I see what you're going for there. I'm going to try an clean up your code to get it as close to running as possible, and then go from there. Let me know if I've made any assumptions that were incorrect.

Assumption: From class CollectionModel<Model> it looks like you're trying to create a collection of things that conform to BaseEvent<M>, where M is the same type for all events. For example, [AddEvent<Int>, RemoveEvent<Int>], would be a valid collection, but [AddEvent<Int>, RemoveEvent<Float>] would not be valid.

If that's the case then what you were likely going for with your cast method was something more like

func cast<Container: BaseEvent<Model>>(event: BaseEvent<Model>) -> Container? {
    return event as? Container // If casting fails, will return nil
}

This would be a method that takes a BaseEvent<Model> and returns another type of event if it can cast to it.

This compiles, but we end up running into the same problem as before

func genericHandler<Event: BaseEvent<Model>>(event: Event) {
    // Desired way to accomplish this
    if let castEvent = cast(event: event) {
        handleEvent(event: castEvent) // prints "ignore base event"
    }
}

unless you provide a more specific type to attempt to cast to, cast(event:) will always default to returning a BaseEvent<>

func genericHandler<Event: BaseEvent<Model>>(event: Event) {
    // Desired way to accomplish this
    if let castEvent: AddEvent<Model> = cast(event: event) { // Change here
        handleEvent(event: castEvent) // prints "add to collection"
    }
}

So not a great position to be in. If you find yourself casting a ton, it usually means you're fighting the type system and there's often a better way. In this case, one thing you could do is flip everything and insert the effects of each event in the event itself.

class CollectionModel<Model> {
    func genericHandler<Event: Handlable>(event: Event) {
        event.handle()
    }
}

protocol Handleable {
    func handle() // handle placed into a protocol for cleanliness, but you could also place it into BaseEvent if you'd like
}

class BaseEvent<M>: Handlable {
    func handle() {
        print("ignore base event")
    }
}

class AddEvent<M>: BaseEvent<M> {
    override func handle() {
        print("add to collection")
    }
}

class RemoveEvent<M>: BaseEvent<M> {
    override func handle() {
        print("remove from collection")
    }
}

let collection = CollectionModel<Int>()

let addEvent = AddEvent<Int>()
let secretAddEvent: BaseEvent<Int> = AddEvent<Int>()

collection.genericHandler(event: secretAddEvent) // prints "add to collection"

I feel that this works better than your initial approach. The effects of each event are contained within the event itself, which is nice for a few OOP related reasons. Casting can get pretty messy so avoiding it completely reduces potential bugs.

Let me know if you have any questions or if I've misunderstood :slight_smile:

1 Like

Thanks for the reply.

We've come full circle twice now it seems. The critical bit is that events only carry data - what gets done when that data is received, is up to the object that receives the event. One of the things it could do is pass the event along to somebody else.

Let's say there are 7 different types of CollectionModels, each handling the same event differently, and some not at all if the generic parameter isn't right (which is absent in your example). Placing the logic inside the event only helps if there is ever one type of CollectionModel, which then begs the question of why I'd bother with having event objects in the first place if life were so simple :slight_smile:

I should've included all that in my initial example. I was so stuck fighting generics as usual, that it escaped me which bits of business logic to include to make my intent clear. Live and learn right? :)

1 Like

Without having read all the responses, based on the original question, it looks like you're looking for higher-kinded types. See this section of the Generics Manifesto.

Can you explain more about how you'd apply higher kinded types to this problem? I'm trying to understand what their use case is right now.

The cast function in question wants to be generic not only over the type of the container element, but also over container type. The kind of an element would be *, container itself is a parameterized type, so its kind would be * -> *, which makes it have a higher kind, that's all.

There is no use-case for them right now, as they are not supported by Swift type system.

Please note that I am not saying this is the only solution to the problem, perhaps there exists another one implementable with what's supported right now.

2 Likes