Dispatch via function overloading

Hello everyone,

I'm interested in dispatching via type resolution in overloaded functions, but I'm not sure how to implement this paradigm correctly. Currently, I have naive straightforward version, which doesn't work:

protocol Custom {}

extension Float: Custom {}
extension Int: Custom {}

func compute(_ x: Int, _ y: Int) -> Int {
  print("Result 0: \(x - y)")
}

func compute(_ x: Float, _ y: Float) -> Int {
  print("Result 1: \(x + y)")
}

func compute(_ x: Int, _ y: Float) {
  print("Result 2: \(Float(x) - y)")
}

func schedule<T: Custom, U: Custom>(x: T, y: U) {
  print("x=\(x) has type \(type(of: x))")
  print("y=\(y) has type \(type(of: y))")
  compute(x, y)
}

I'm getting this error, which is slightly confusing, as I have already extended Int and Float types with Custom protocol. Theoretically, the compiler should pick up this information and allow to 'custom' variables to be passed to the overloaded compute function.

error: repl.swift:21:3: error: cannot invoke 'compute' with an argument list of type '(T, U)'
  compute(x, y)
  ^

repl.swift:21:3: note: overloads for 'compute' exist with these partially matching parameter lists: (Int, Int), (Float, Float), (Int, Float)
  compute(x, y)
  ^

Any help is appreciated :slight_smile:

Swift version:

$ swift --version
Apple Swift version 4.2.1 (swiftlang-1000.0.42 clang-1000.10.45.1)
Target: x86_64-apple-darwin18.2.0

When you write a generic function in Swift, you are making a universal declaration. That is, "for all types T and U that conform to Custom, you can call this function schedule passing in a T and a U".

The Swift type system will ensure this is the case. Suppose you also added a conformance String: Custom but then didn't provide an implementation of compute for strings. There would be no compute function to call within the body of schedule. The compiler won't let this happen.

The compiler does this only through the method signatures; it does not look at the implementation of a generic function to check everything is OK. Instead, it requires that the only capabilities used in a generic function's body on generic types must come from the constraints you put on the generic types – in this case, other functions or properties made available by your Custom constraint.

The easiest way to resolve this is to make compute a requirement of the protocol. That way, the only way you can make Float conform to Custom is if you provide that function. And that means functions that constrain to Custom are guaranteed to be able to call it:

protocol Custom {
  static func compute(_ x: Self, _ y: Self) -> Self
}

Now, if you try and write extension Float: Custom { } you'll get a compiler error telling you Float doesn't conform, because you haven't implemented custom. If you do, it'll compile, and you could use it to implement schedule:

extension Float: Custom {
  static func compute(_ x: Float, _ y: Float) -> Float {
    return x+y
  }
}

func schedule<T: Custom>(x: T, y: T) {
  T.compute(x, y)
}

Unfortunately, this still doesn't do what you want because this assumes x and y are of the same type. You want multiple dispatch – to drive different logic from two different types. Implementing this can be a pain, because you need to cater for every possible combination, and how to best do it really depends on what you're trying to achieve. I'd suggest googling "multiple dispatch" for ideas. Though there's not much material about Swift, so you'll have to adapt patterns from C++ or Java articles, so you may want to familiarize yourself with Swift generics first with simpler single-dispatch cases.

8 Likes

As Ben sketched out, it's rather more complicated than this; All that schedule knows is that T and U conform to the Custom protocol. There's no way to get from that to "T and U must be either Float or Int." All that the compiler can conclude is that T and U implement all requirements of Custom, which is to say they could be anything at all, since Custom has no requirements.

This is all dancing around a really critical thing to understand about generics--they look like templates, but they are actually wildly different things. (C++) templates are always specialized at compile time, and are approximately doing textual substitution with some scoping rules to make it more pleasant than C-style macros. This kind of approach would work in C++, because by the time the compiler is generating code for schedule, it knows the type of x and y and can just plonk in the appropriate overload. It does not work with generics, because the compiler has to generate an implementation that works for any T and U conforming to Custom, not only the concrete types that you happen to want to use it for. There are advantages as well as drawbacks to this, but it's really important to understand that they're different things, and require different patterns.

6 Likes

Fwiw:

protocol Custom {}

extension Float: Custom {}
extension Int: Custom {}

func schedule<T: Custom, U: Custom>(x: T, y: U) {
    print("x=\(x) has type \(type(of: x))")
    print("y=\(y) has type \(type(of: y))")

    if let (x, y) = (x, y) as? (Int, Int) {
        print("Result 0: \(x - y)") // Or you could call your compute(x, y) here ...
    }
    else if let (x, y) = (x, y) as? (Float, Float) {
        print("Result 1: \(x + y)") // ... and here ...
    }
    else if let (x, y) = (x, y) as? (Int, Float) {
        print("Result 2: \(Float(x) - y)") // ... and here.
    }
    else {
        preconditionFailure("Unsupported")
    }
}

schedule(x: 1, y: 2)
schedule(x: Float(3.4), y: Float(4.5))
schedule(x: 6, y: Float(7.8))

Will print:

x=1 has type Int
y=2 has type Int
Result 0: -1
x=3.4 has type Float
y=4.5 has type Float
Result 1: 7.9
x=6 has type Int
y=7.8 has type Float
Result 2: -1.8000002