Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why doesn’t Swift call my overloaded method with a more specific type?

I use Decodable to decode a simple struct from JSON. This works by conforming to a Decodable protocol:

extension BackendServerID: Decodable {

    static func decode(_ json: Any) throws -> BackendServerID {
        return try BackendServerID(
            id: json => "id",
            name: json => "name"
        )
    }
}

I’d like to be able to call decode with a String, though, so I have added an extension:

extension Decodable {

    static func decode(_ string: String) throws -> Self {
        let jsonData = string.data(using: .utf8)!
        let jsonObject = try JSONSerialization.jsonObject(with: jsonData, options: [])
        return try decode(jsonObject)
    }
}

Then I would like to decode the objects like this:

XCTAssertNoThrow(try BackendServerID.decode("{\"id\": \"foo\", \"name\": \"bar\"}"))

This doesn’t work as expected, though, since somehow the decode(Any) method gets called instead of decode(String). What am I doing wrong? (When I clarify the call by renaming my custom method to decodeString, it works correctly.)

like image 506
zoul Avatar asked May 05 '17 09:05

zoul


2 Answers

I would agree that this behaviour is surprising, and you may well want to file a bug over it.

From quickly looking through the source of CSRanking.cpp, which is the part of type checker implementation that deals with the "rankings" for different declarations when it comes to overload resolution – we can see that in the implementation of:

/// \brief Determine whether the first declaration is as "specialized" as
/// the second declaration.
///
/// "Specialized" is essentially a form of subtyping, defined below.
static bool isDeclAsSpecializedAs(TypeChecker &tc, DeclContext *dc,
                                  ValueDecl *decl1, ValueDecl *decl2) {

The type checker considers an overload in a concrete type to be more "specialised" than an overload in a protocol extension (source):

  // Members of protocol extensions have special overloading rules.
  ProtocolDecl *inProtocolExtension1 = outerDC1
                                         ->getAsProtocolExtensionContext();
  ProtocolDecl *inProtocolExtension2 = outerDC2
                                         ->getAsProtocolExtensionContext();
  if (inProtocolExtension1 && inProtocolExtension2) {
    // Both members are in protocol extensions.
    // Determine whether the 'Self' type from the first protocol extension
    // satisfies all of the requirements of the second protocol extension.
    bool better1 = isProtocolExtensionAsSpecializedAs(tc, outerDC1, outerDC2);
    bool better2 = isProtocolExtensionAsSpecializedAs(tc, outerDC2, outerDC1);
    if (better1 != better2) {
      return better1;
    }
  } else if (inProtocolExtension1 || inProtocolExtension2) {
    // One member is in a protocol extension, the other is in a concrete type.
    // Prefer the member in the concrete type.
    return inProtocolExtension2;
  }

And when performing overload resolution, the type checker will keep track of a "score" for each potential overload, picking the one with the highest. When a given overload is considered more "specialised" than another, its score will be incremented, therefore meaning that it will be favoured. There are other factors that can affect an overload's score, but isDeclAsSpecializedAs appears to be the deciding factor in this particular case.

So, if we consider a minimal example, similar to the one @Sulthan gives:

protocol Decodable {
    static func decode(_ json: Any) throws -> Self
}

struct BackendServerID {}

extension Decodable {
    static func decode(_ string: String) throws -> Self {
        return try decode(string as Any)
    }
}

extension BackendServerID : Decodable {
    static func decode(_ json: Any) throws -> BackendServerID {
        return BackendServerID()
    }
}

let str = try BackendServerID.decode("foo")

When calling BackendServerID.decode("foo"), the overload in the concrete type BackendServerID is preferred to the overload in the protocol extension (the fact that the BackendServerID overload is in an extension of the concrete type doesn't make a difference here). In this case, this is regardless of whether one is more specialised when it comes to the function signature itself. The location matters more.

(Although the function signature does matter if generics are involved – see tangent below)

It's worth noting that in this case we can force Swift to use the overload we want by casting the method at the call:

let str = try (BackendServerID.decode as (String) throws -> BackendServerID)("foo")

This will now call the overload in the protocol extension.

If the overloads were both defined in BackendServerID:

extension BackendServerID : Decodable {
    static func decode(_ json: Any) throws -> BackendServerID {
        return BackendServerID()
    }

    static func decode(_ string: String) throws -> BackendServerID {
        return try decode(string as Any)
    }
}

let str = try BackendServerID.decode("foo")

The the above condition in the type checker implementation won't be triggered, as neither are in a protocol extension – therefore when it comes to overload resolution, the more "specialised" overload will be solely based on signatures. Therefore the String overload will be called for a String argument.


(Slight tangent regarding generic overloads...)

It's worth noting that there are (lots of) other rules in the type checker for whether one overload is considered more "specialised" than another. One of these is preferring non-generic overloads to generic overloads (source):

  // A non-generic declaration is more specialized than a generic declaration.
  if (auto func1 = dyn_cast<AbstractFunctionDecl>(decl1)) {
    auto func2 = cast<AbstractFunctionDecl>(decl2);
    if (func1->isGeneric() != func2->isGeneric())
      return func2->isGeneric();
  }

This condition is implemented higher up than the protocol extension condition – therefore if you were to change the decode(_:) requirement in the protocol such that it used a generic placeholder:

protocol Decodable {
    static func decode<T>(_ json: T) throws -> Self
}

struct BackendServerID {}

extension Decodable {
    static func decode(_ string: String) throws -> Self {
        return try decode(string as Any)
    }
}

extension BackendServerID : Decodable {
    static func decode<T>(_ json: T) throws -> BackendServerID {
        return BackendServerID()
    }
}

let str = try BackendServerID.decode("foo")

The String overload will now be called instead of the generic one, despite being in a protocol extension.


So really, as you can see, there are lots of complicated factors that determine which overload to call. Really the best solution in this case, as others have already said, is to explicitly disambiguate the overloads by giving your String overload an argument label:

extension Decodable {
    static func decode(jsonString: String) throws -> Self {
        // ...
    }
}

// ...

let str = try BackendServerID.decode(jsonString: "{\"id\": \"foo\", \"name\": \"bar\"}")

Not only does this clear up the overload resolution, it also makes the API clearer. With just decode("someString"), it wasn't clear exactly what format the string should be in (XML? CSV?). Now it's perfectly clear that it expects a JSON string.

like image 142
Hamish Avatar answered Oct 04 '22 16:10

Hamish


Let's consider the minimal example:

protocol Decodable {
    static func decode(_ json: Any) throws -> Self
}

struct BackendServerID {
}

extension Decodable {
    static func decode(_ string: String) throws -> Self {
        return try decode(string)
    }
}

extension BackendServerID : Decodable {
    static func decode(_ json: Any) throws -> BackendServerID {
        return BackendServerID()
    }
}

The implementation of decode in BackendServerId replaces the default implementation of Decodable.decode (the parameters are covariant, similar case as overriding). Your use case would work only if both functions were declared on the same level, e.g.:

extension BackendServerID : Decodable {
    static func decode(_ json: Any) throws -> BackendServerID {
        return BackendServerID()
    }

    static func decode(_ string: String) throws -> Self {
        return try decode(string as Any)
    }
}

Also note the as Any which is necessary to prevent recursion.

To prevent confusion, you should name functions that accept string and Any differently, e.g decode(string:) and decode(json:).

like image 26
Sulthan Avatar answered Oct 04 '22 16:10

Sulthan