Why is this property setter appearing to lose the type information?

In the sample code below, which can be pasted into a Playground, why is the property setter calling Preferences.save(_ Any?) and not Preferences.save(_ Vehicle)?

If you stop in the debugger, newValue is a Vehicle. If you don't use a property setter, then the code behaves as expected.

Xcode Version 12.2 (12B45b)

import Foundation

enum Vehicle: Int {
  case bus
  case car
}

enum Preferences {
  static var storage = [String : Any?]()
  
  static func save(_ value: Vehicle, key: String) {
    print("Saving Vehicle")
  }
  
  static func save(_ value: Any?, key: String) {
    print("Saving Any?")
  }
}

struct Rental<T> {
  var vehicle: T {
    get { /* Ignore */ Preferences.storage["vehicle"] as! T }
    set { Preferences.save(newValue, key: "vehicle")}
  }
}

// This calls Preferences.save(Vehicle)
Preferences.save(Vehicle.bus, key: "vehicle")

// This calls Preferences.save(Any?)
var person = Rental<Vehicle>()
person.vehicle = .bus

Understanding this behavior is pretty key to using overloading and generics correctly in Swift.

First, you don't unlock any magic by giving two functions the same name. Overloading simply means that Swift has to figure out, at compile time, which of the two functions the user means to use, at the point where that name is mentioned. (Both of these italicized points are essential to keep in mind.)

At the end of the day, however, we're just working with two distinct functions. So, let's do this manually as though we're the compiler. First, let's give your two functions distinct names so that we can refer to them unambiguously:

  static func saveVehicle(_ value: Vehicle, key: String) {
    print("Saving Vehicle")
  }
  
  static func saveAny(_ value: Any?, key: String) {
    print("Saving Any?")
  }

In reality, for a module Example containing your original code, the Swift compiler mangles the names of these two functions named save(_:key:), not as saveVehicle and saveAny, but as $s7Example11PreferencesO4save_3keyyAA7VehicleO_SStFZ and $s7Example11PreferencesO4save_3keyyypSg_SStFZ.

Next, let's figure out which one the user means to use, at compile time and at the point where the name is mentioned:

  var vehicle: T {
    get { /* Ignore */ Preferences.storage["vehicle"] as! T }
    set { Preferences.saveAny(newValue, key: "vehicle") }
  }

Could the user instead write saveVehicle here? Impossible: that would not typecheck, because you cannot call saveVehicle for an arbitrary value of type T.

You can check that the Swift compiler is actually doing the equivalent of this when it resolves overloads: simply paste your original code into a file and compile it with the -emit-sil -module-name="Example" options, and you can see that the setter includes the following:

  // function_ref static Preferences.save(_:key:)
  %16 = function_ref @$s7Example11PreferencesO4save_3keyyypSg_SStFZ : $@convention(method) (@in_guaranteed Optional<Any>, @guaranteed String, @thin Preferences.Type) -> () // user: %17
  %17 = apply %16(%5, %15, %4) : $@convention(method) (@in_guaranteed Optional<Any>, @guaranteed String, @thin Preferences.Type) -> ()

This behavior is to be distinguished from dynamic dispatch, where Swift chooses the most specific overload at runtime. In Swift, there are only three ways to get type-based dynamic dispatch:

19 Likes

Fantastic reply. Thanks for taking the time to explain it so clearly.

I seldom work with generics but before reading your explanation and by looking at the code sample alone I'd expect Swift to generate struct RentalVehicle (with T replaced with Vehicle everywhere) before deciding which overload to take thus picking save(_:Vehicle) and not save(_:Any).

Is this of the differences between Swift's generics and C++'s templates:

void save(Vehicle *value) { std::cout << "save Vehicle" << std::endl; }
void save(void *value) { std::cout << "save Any" << std::endl; }

template<typename T> struct Rental {
    void setVehicle(T newValue) { save(&newValue); }
};

int main()
{
    auto rental = Rental<Vehicle>();
    auto car = Vehicle();
    rental.setVehicle(car);
}

>>> save Vehicle