6

I have code that follows the general design of:

protocol DispatchType {}
class DispatchType1: DispatchType {}
class DispatchType2: DispatchType {}

func doBar<D:DispatchType>(value:D) {
    print("general function called")
}

func doBar(value:DispatchType1) {
    print("DispatchType1 called")
}

func doBar(value:DispatchType2) {
    print("DispatchType2 called")
}

where in reality DispatchType is actually a backend storage. The doBarfunctions are optimized methods that depend on the correct storage type. Everything works fine if I do:

let d1 = DispatchType1()
let d2 = DispatchType2()

doBar(value: d1)    // "DispatchType1 called"
doBar(value: d2)    // "DispatchType2 called"

However, if I make a function that calls doBar:

func test<D:DispatchType>(value:D) {
    doBar(value: value)
}

and I try a similar calling pattern, I get:

test(value: d1)     // "general function called"
test(value: d2)     // "general function called"

This seems like something that Swift should be able to handle since it should be able to determine at compile time the type constraints. Just as a quick test, I also tried writing doBar as:

func doBar<D:DispatchType>(value:D) where D:DispatchType1 {
    print("DispatchType1 called")
}

func doBar<D:DispatchType>(value:D) where D:DispatchType2 {
    print("DispatchType2 called")
}

but get the same results.

Any ideas if this is correct Swift behavior, and if so, a good way to get around this behavior?

Edit 1: Example of why I was trying to avoid using protocols. Suppose I have the code (greatly simplified from my actual code):

protocol Storage {
     // ...
}

class Tensor<S:Storage> {
    // ...
}

For the Tensor class I have a base set of operations that can be performed on the Tensors. However, the operations themselves will change their behavior based on the storage. Currently I accomplish this with:

func dot<S:Storage>(_ lhs:Tensor<S>, _ rhs:Tensor<S>) -> Tensor<S> { ... }

While I can put these in the Tensor class and use extensions:

extension Tensor where S:CBlasStorage {
    func dot(_ tensor:Tensor<S>) -> Tensor<S> {
       // ...
    }
}

this has a few side effects which I don't like:

  1. I think dot(lhs, rhs) is preferable to lhs.dot(rhs). Convenience functions can be written to get around this, but that will create a huge explosion of code.

  2. This will cause the Tensor class to become monolithic. I really prefer having it contain the minimal amount of code necessary and expand its functionality by auxiliary functions.

  3. Related to (2), this means that anyone who wants to add new functionality will have to touch the base class, which I consider bad design.

Edit 2: One alternative is that things work expected if you use constraints for everything:

func test<D:DispatchType>(value:D) where D:DispatchType1 {
    doBar(value: value)
}

func test<D:DispatchType>(value:D) where D:DispatchType2 {
    doBar(value: value)
}

will cause the correct doBar to be called. This also isn't ideal, as it will cause a lot of extra code to be written, but at least lets me keep my current design.

Edit 3: I came across documentation showing the use of static keyword with generics. This helps at least with point (1):

class Tensor<S:Storage> {
   // ...
   static func cos(_ tensor:Tensor<S>) -> Tensor<S> {
       // ...
   }
}

allows you to write:

let result = Tensor.cos(value)

and it supports operator overloading:

let result = value1 + value2

it does have the added verbosity of required Tensor. This can made a little better with:

typealias T<S:Storage> = Tensor<S>
Abe Schneider
  • 977
  • 1
  • 11
  • 23
  • This happens because of the way Swift implements methods dispatch. Please have a look at https://www.raizlabs.com/dev/2016/12/swift-method-dispatch/ - especially the chapter Reference Type Matters¨. – 0x416e746f6e Feb 01 '17 at 12:53
  • Related: [Extending Collection with a recursive property/method that depends on the element type](http://stackoverflow.com/q/41640321/2976878) – Hamish Feb 01 '17 at 13:10
  • Somewhat related: [How to call the more specific method of overloading](http://stackoverflow.com/questions/41531569/how-to-call-the-more-specific-method-of-overloading) – dfrib Feb 01 '17 at 14:21

1 Answers1

8

This is indeed correct behaviour as overload resolution takes place at compile time (it would be a pretty expensive operation to take place at runtime). Therefore from within test(value:), the only thing the compiler knows about value is that it's of some type that conforms to DispatchType – thus the only overload it can dispatch to is func doBar<D : DispatchType>(value: D).

Things would be different if generic functions were always specialised by the compiler, because then a specialised implementation of test(value:) would know the concrete type of value and thus be able to pick the appropriate overload. However, specialisation of generic functions is currently only an optimisation (as without inlining, it can add significant bloat to your code), so this doesn't change the observed behaviour.

One solution in order to allow for polymorphism is to leverage the protocol witness table (see this great WWDC talk on them) by adding doBar() as a protocol requirement, and implementing the specialised implementations of it in the respective classes that conform to the protocol, with the general implementation being a part of the protocol extension.

This will allow for the dynamic dispatch of doBar(), thus allowing it to be called from test(value:) and having the correct implementation called.

protocol DispatchType {
    func doBar()
}

extension DispatchType {
    func doBar() {
        print("general function called")
    }
}

class DispatchType1: DispatchType {
    func doBar() {
        print("DispatchType1 called")
    }
}

class DispatchType2: DispatchType {
    func doBar() {
        print("DispatchType2 called")
    }
}

func test<D : DispatchType>(value: D) {
    value.doBar()
}

let d1 = DispatchType1()
let d2 = DispatchType2()

test(value: d1)    // "DispatchType1 called"
test(value: d2)    // "DispatchType2 called"
Hamish
  • 78,605
  • 19
  • 187
  • 280
  • It might be intended behavior, but I definitely wouldn't have expected it! – Andreas Feb 01 '17 at 12:56
  • I agree it isn't expected behavior, at least coming from c++ templates. Is there a way to force the compiler to create separate versions and use them for resolution (I tried `@_specialize`, but that didn't work)? I know if I make non-generic versions of `test` it works, so in theory Swift is capable of behaving correctly. Unfortunately, using protocols makes the design very ugly, so I rather avoid doing that... – Abe Schneider Feb 01 '17 at 13:12
  • @AbeSchneider I don't *believe* there's currently a way to force specialisation of a given generic function – it's simply done as an optimisation by the compiler. Although it might be worth [filing for an improvement](https://bugs.swift.org) to see if the Swift team would consider it. Given that your original code uses a protocol, I'm not sure what you find so ugly about the approach of adding `doBar()` as a requirement – although there may well be a better solution for your concrete use case (possibly typecasting). Although it's hard to say without seeing a concrete example. – Hamish Feb 01 '17 at 13:23
  • @AbeSchneider Also see [the comments of this bug report](https://bugs.swift.org/browse/SR-1416) where Jordan Rose compares Swift's generics to C++'s templates. – Hamish Feb 01 '17 at 13:25
  • @Hamish Awesome, thanks for the help! As for the 'ugly', the code I'm writing is supposed to allow other people to easily create new functions without having to touch the main code. Having to everything in protocols violates that. I'll include an example above. If they modify how extensions to work, then it would be less bad, because then users could at least keep their code separate. – Abe Schneider Feb 02 '17 at 01:57
  • 1
    A improvement suggestion was added: https://bugs.swift.org/browse/SR-3829. I can see why this may not be the desired behavior, but if an attribute were added to force the compiler to do something it already does to support these cases, it might be a nice compromise. – Abe Schneider Feb 02 '17 at 03:34
  • @AbeSchneider Yeah, that's a shame :/ – Hamish Feb 03 '17 at 21:01