Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Generic method override not working in swift

There is a protocol Printable and a struct Printer from a 3rd Party.

protocol Printable {}

struct Printer {

    static func print<T>(object: T) -> String {
        return "T"
    }

    static func print<T: Printable>(object: T) -> String {
        return "Printable"
    }

}

Now i am making a generic

struct Generic<T> {

    var args: T

    func display() {
        print(Printer.print(args))
    }

}

and two structs

struct Obj {}
struct PrintableObj: Printable {}
var obj = Generic(args: Obj())
var printableObj = Generic(args: PrintableObj())

When i call the display functions on both of them.

obj.display()

displays T

printableObj.display()

displays T but i want it to print "Printable"

One solution i can think of is having two different generics

struct Generic<T>
struct PrintableGeneric<T: Printable>

Is there any other solution without changing the Printable protocol and Printer struct.

like image 884
Rahul Katariya Avatar asked Feb 16 '16 11:02

Rahul Katariya


3 Answers

static func print<T>(object: T) -> String {
    if object is Printable {
        return "Printable"
    } else {
        return "T"
    }
}
like image 72
user3441734 Avatar answered Oct 20 '22 18:10

user3441734


Yes. But the answer is a bit weird. The first part makes a decent amount of sense; the second part is just totally weird. Let's walk through it.

struct Generic<T> {
    var args: T
    func display() {
        print(Printer.print(args))
    }
}

The correct overload to choose for print is decided at compile time, not runtime. This is the thing that confuses people the most. They want to treat Swift like JavaScript where everything is dynamic. Swift likes to be static because then it can make sure your types are right and it can do lots of optimizations (and Swift loves to do compiler optimizations). So, compile time, what type is args? Well, it's T. Is T known to be Printable? No it is not. So it uses the non-Printable version.

But when Swift specializes Generic using PrintableObj, doesn't it know at that point that it's Printable? Couldn't the compiler create a different version of display at that point? Yes, if we knew at compile time every caller that would ever exist of this function, and that none of them would ever be extended to be Printable (which could happen in a completely different module). It's hard to solve this without creating lots of weird corner cases (where internal things behave differently than public things for instance), and without forcing Swift to proactively generate every possible version of display that might be required by some future caller. Swift may improve in time, but this is a hard problem I think. (Swift already suffers some performance reductions so that public generics can be specialized without access to the original source code. This would make that problem even more complicated.)

OK, so we get that. T isn't Printable. But what if we had a type that was unambiguously Printable that we knew at compile time and lived inside this function? Would it work then?

func display() {
    if let p = args as? Printable {
        print(Printer.print(p))
    } else {
        print(Printer.print(args))
    }
}

Oh so close... but not quite. This almost works. The if-let actually does exactly what you want it to do. p gets assigned. It's Printable. But it still calls the non-Printable function. ?!?!?!?!

This is a place I personally think that Swift is just currently broken and have high hopes it will be fixed. It might even be a bug. The problem is that Printable itself does not conform to Printable. Yeah, I don't get it either, but there you go. So we need to make something that does conform to Printable in order to get the right overload. As usual, type erasers to the rescue.

struct AnyPrintable: Printable {
    let value: Printable
}

struct Generic<T> {
    var args: T

    func display() {
        if let p = args as? Printable {
            print(Printer.print(AnyPrintable(value: p)))
        } else {
            print(Printer.print(args))
        }
    }
}

And this will print the way you wanted. (On the assumption that Printable requires some methods, you'd just add those methods to the AnyPrintable type eraser.)

Of course the right answer is not to use generic overloads this way in Printer. It's just way too confusing and fragile. It looks so nice, but it blows up all the time.

like image 24
Rob Napier Avatar answered Oct 20 '22 17:10

Rob Napier


To my mind, the only option you have - is to use if-else with type casting in "print()" function

static func print<T>(object: T) -> String {
    if let _ = object as? Printable {
        return "Printable"
    }
    return "T"
}

or non-generic variant

static func print(object: Any) -> String {
    if let _ = object as? Printable {
        return "Printable"
    }
    return "T"
}
like image 1
Anton Belousov Avatar answered Oct 20 '22 18:10

Anton Belousov