Multiple Dispatch Swift attempt: Conflicting arguments to generic parameter 'T' ('Cat' vs. 'Dog')

7.7k views Asked by At

I was watching a video on Julia about Multiple Dispatch and was curious if I could write something similar in Swift. I see that Swift relies on the compiler where Julia seems to determine a type at run time, but I also discovered something I don't understand about Swift.

Why would the below function encounters work when the 2 parameters are of the same type of Pet but not when one is a Cat and the other is a Dog?

For example the two functions below work

encounters(Arturo, Gabby) // both Cat
encounters(Cc, Bb) // both Dog

but these give a compiler error

Conflicting arguments to generic parameter 'T' ('Cat' vs. 'Dog')

encounters(Arturo, Bb) // Cat and Dog
encounters(Bb, Arturo) // Dog and Cat
protocol Pet: Equatable {
    var name: String { get }
}
struct Cat: Pet {
    let name: String
}
struct Dog: Pet {
    let name: String
}

let Arturo = Cat(name: "Arturo")
let Gabby = Cat(name: "Gabby")
let Bb = Dog(name: "Bb")
let Cc = Dog(name: "Cc")

func encounters<T: Pet>(_ a: T, _ b: T) {
    var verb: String
    switch (a, b) {
    case is (Cat, Dog):
        verb = meet(a as! Cat, b as! Dog)
    case is (Dog, Dog):
        verb = meet(a as! Dog, b as! Dog)
    case is (Cat, Cat):
        verb = meet(a as! Cat, b as! Cat)
    case is (Dog, Cat):
        verb = meet(a as! Dog, b as! Cat)
        
    default:
        fatalError()
    }
    
    print("\(a.name) meets \(b.name) and \(verb)")
}

func meet(_ a: Cat, _ b: Cat) -> String {
    return "Slinks"
}

func meet(_ a: Cat, _ b: Dog) -> String {
    return "Hisses"
}

func meet(_ a: Dog, _ b: Dog) -> String {
    return "Howles"
}

func meet(_ a: Dog, _ b: Cat) -> String {
    return "Barks"
}
2

There are 2 answers

2
Rob Napier On BEST ANSWER

I would not take this approach, but the reason it doesn't work is because you attached Equatable to Pet. You probably meant something like "Pets should be comparable to other Pets," but that's not what that means. It means that types that conform to Pet must themselves be Equatable. If you remove Equatable, this works as written without the generic as Claus notes:

protocol Pet { ... }
func encounters(_ a: Pet, _ b: Pet) { ... }

So much as! makes Swift devs nervous and is unnecessary. The as! isn't wrong, but it opens the door for easy mistakes that the compiler can't catch. This style lets the compiler help you a little more. It still won't be able to catch missing cases.

func encounters(_ a: Pet, _ b: Pet) {
    var verb: String
    switch (a, b) {
    case let (a as Cat, b as Dog):
        verb = meet(a, b)
    case let (a as Dog, b as Dog):
        verb = meet(a, b)
    case let (a as Cat, b as Cat):
        verb = meet(a, b)
    case let (a as Dog, b as Cat):
        verb = meet(a, b)

    default:
        fatalError()
    }

    print("\(a.name) meets \(b.name) and \(verb)")
}

I definitely do not like the fatalError here. Both this code and the Julia I assume you've written would crash if some new Pet were created. That's not very good. But I assume it's not a major part of this question. In Swift, you'd put something in the default leg. In Julia, you'd add a more general multimethod for meet(a::Pet, b::Pet).

If there really are only the two possible Pets, then you really should be thinking about enums instead of dynamic dispatch, but again I assume that's not the question.

Like I said, I don't really like this approach. I think it's very verbose, and adds a lot of places for mistakes. If you want this kind of dynamic type lookup, I'd do dynamic type lookup in data.

struct Meeting {
    let lhs: Pet.Type
    let rhs: Pet.Type
    let verb: String

    func matches(_ lhs: Pet, _ rhs: Pet) -> Bool {
        self.lhs == type(of: lhs) && self.rhs == type(of: rhs)
    }
}

let meetings = [
    Meeting(lhs: Cat.self, rhs: Dog.self, verb: "Hisses"),
    Meeting(lhs: Dog.self, rhs: Dog.self, verb: "Howles"),
    Meeting(lhs: Cat.self, rhs: Cat.self, verb: "Slinks"),
    Meeting(lhs: Dog.self, rhs: Cat.self, verb: "Barks"),
]

func encounters(_ a: Pet, _ b: Pet) {
    let verb = meetings
        .first(where: { $0.matches(a, b) })?.verb
        ?? "passes by"
    print("\(a.name) meets \(b.name) and \(verb)")
}

Even if you need functions, you can expand this to storing functions as data (which is an incredibly powerful way of implementing dynamic dispatch).

Instead of just a verb, add an action function to Meeting:

struct Meeting {
    let lhs: Pet.Type
    let rhs: Pet.Type
    let action: (Pet) -> String  // Function that takes a Pet and gives a String

    func matches(_ lhs: Pet, _ rhs: Pet) -> Bool {
        self.lhs == type(of: lhs) && self.rhs == type(of: rhs)
    }
}

Now it's starting to look more like your multi-methods, putting all the specialized logic in one place:

let meetings = [
    Meeting(lhs: Cat.self, rhs: Dog.self, action: { "Hisses at \($0.name)" }),
    Meeting(lhs: Dog.self, rhs: Dog.self, action: { "Howles at \($0.name)" }),
    Meeting(lhs: Cat.self, rhs: Cat.self, action: { _ in "Slinks by" }),
    Meeting(lhs: Dog.self, rhs: Cat.self, action: { "Barks at \($0.name)" }),
]

And encounters calls it:

func encounters(_ a: Pet, _ b: Pet) {
    let action = meetings
        .first(where: { $0.matches(a, b) })?.action
        ?? { _ in "passes by" }

    let verb = action(b)
    
    print("\(a.name) meets \(b.name) and \(verb)")
}

It's not completely as powerful as multimethods, but it's easier to reason about the type safety at compile-time.

2
Joakim Danielson On

If you are going to create a generic function with a type that conforms to a protocol then you need to use that protocol all the way, you can't use conforming types inside the function since any custom type can be made to conform to the protocol.

Here is an example solution using the protocol only

protocol Pet {
    var name: String { get }
    var same: String { get }
    var other: String { get }
}

Cat and Dog conforming to the protocol

struct Cat: Pet {
    var name: String
    var other: String { "Slinks" }
    var same: String { "Hisses" }
}
struct Dog: Pet {
    let name: String
    var other: String { "Barks" }
    var same: String { "Howles" }
}

and then the function becomes

func encounters<T: Pet, U: Pet>(_ a: T, _ b: U) {
    let verb: String
    if T.self == U.self {
        verb = a.same
    } else {
        verb = a.other
    }
    print("\(a.name) meets \(b.name) and \(verb)")
}

example

let arturo = Cat(name: "Arturo")
let gabby = Cat(name: "Gabby")
let bb = Dog(name: "Bb")
let cc = Dog(name: "Cc")

encounters(arturo, bb)
encounters(bb, arturo)
encounters(gabby, arturo)
encounters(cc, bb)

Arturo meets Bb and Slinks
Bb meets Arturo and Barks
Gabby meets Arturo and Hisses
Cc meets Bb and Howles