Generic functions overload in Protocols

Hello everybody, my first post here, I didn't find anything similar. Hope I didn't miss.

I'm trying to build a protocol to check if an Entity should be saved or not by the implementer.
So the protocol acts as blueprint for the implementer.

The main goal is not to associate Entity: Equatable to the protocol, I really need to use it with anything (i.e in memory caching always replacing the old object with a new one, example caching an AnyObject)

So to achieve this I want the implementer to take decision on it, at the same time, to facilitate the implementer, default implementations for specific conforming types are provided.

As result using the protocol function shouldUpdate directly all works as expected.
When instead the implementation detail use it's own shouldUpdate function it fails to use the proper default implementation.
The one with no where constraints is always used.

To my knowledge Swift should always use the most restrictive overloading signature as long a Entity is well defined, and not erased (i.e Any) but apparently it doesn't, at least in this case.

I'm clearly missing something and need some clarification on how Swift select the overloading function to use :pray:

Code + Test following.
Environment: Xcode 16.0 (16A242d) && 16.1 (16B40) + MacOS. 14.7.1

import Foundation
import Testing

public protocol ShouldUpdateProtocol {
  func shouldUpdate<Entity>(_ new: Entity, replacing old: Entity) -> Bool
  func shouldUpdate<Entity>(_ new: Entity, replacing old: Entity) -> Bool where Entity: AnyObject
  func shouldUpdate<Entity>(_ new: Entity, replacing old: Entity) -> Bool where Entity: Identifiable
  func shouldUpdate<Entity>(_ new: Entity, replacing old: Entity) -> Bool where Entity: Equatable
  func shouldUpdate<Entity>(_ new: Entity, replacing old: Entity) -> Bool where Entity: Equatable & Identifiable
}

public extension ShouldUpdateProtocol {
  func shouldUpdate<Entity>(_ new: Entity, replacing old: Entity) -> Bool { true }
}

public extension ShouldUpdateProtocol {
  func shouldUpdate<Entity>(_ new: Entity, replacing old: Entity) -> Bool where Entity: AnyObject { new !== old }
}

public extension ShouldUpdateProtocol {
  func shouldUpdate<Entity>(_ new: Entity, replacing old: Entity) -> Bool where Entity: Identifiable { new.id != old.id }
}

public extension ShouldUpdateProtocol {
  func shouldUpdate<Entity>(_ new: Entity, replacing old: Entity) -> Bool where Entity: Equatable { new != old }
}

public extension ShouldUpdateProtocol {
  func shouldUpdate<Entity>(_ new: Entity, replacing old: Entity) -> Bool where Entity: Equatable & Identifiable { new != old }
}



struct TestShouldUpdate<T>: ShouldUpdateProtocol {

  let currentValue: T

  func save(_ value: T) -> Bool {
    self.shouldUpdate(value, replacing: self.currentValue)
  }
}

@Suite
struct ShoudlUpdateTests {

  @Suite("Given_AShouldUpdate_Equatable_Case")
  struct SectionOne {

    @Test
    func When_CallingShouldUpdateWithSameValue_Then_ItReturnsFalse() async throws {
      let sut = TestShouldUpdate(currentValue: 5)

      let saved = sut.save(5)
      let shouldUpdate = sut.shouldUpdate(5, replacing: sut.currentValue)

      #expect(saved == false)        // πŸ’₯ Expectation failed: (saved β†’ true) == false
      #expect(shouldUpdate == false) // βœ…
    }

    @Test
    func When_CallingShouldUpdateWithDifferentValue_Then_ItReturnsTrue() async throws {
      let sut = TestShouldUpdate(currentValue: 5)

      let saved = sut.save(10)
      let shouldUpdate = sut.shouldUpdate(10, replacing: sut.currentValue)

      #expect(saved == true)
      #expect(shouldUpdate == true)
    }
  }
  @Suite("Given_AShouldUpdate_Identifiable_Case")
  struct SectionTwo {

    struct TestIdentifiable: Identifiable {
      let id: UUID
      var text: String = "Test"
    }

    @Test
    func When_CallingShouldUpdateWithSameValue_Then_ItReturnsFalse() async throws {
      let testObject = TestIdentifiable(id: UUID())
      let sut = TestShouldUpdate(currentValue: testObject)

      let saved = sut.save(testObject)
      let shouldUpdate = sut.shouldUpdate(testObject, replacing: sut.currentValue)

      #expect(saved == false)        // πŸ’₯ Expectation failed: (saved β†’ true) == false
      #expect(shouldUpdate == false) // βœ…
    }

    @Test
    func When_CallingShouldUpdateWithModifiedValueButSameId_Then_ItReturnsFalse() async throws {
      let testObject = TestIdentifiable(id: UUID())
      let sut = TestShouldUpdate(currentValue: testObject)

      var modifiedTestObjectWithSameId = testObject
      modifiedTestObjectWithSameId.text = "Changed"

      let saved = sut.save(modifiedTestObjectWithSameId)
      let shouldUpdate = sut.shouldUpdate(modifiedTestObjectWithSameId, replacing: sut.currentValue)

      #expect(saved == false)        // πŸ’₯ Expectation failed: (saved β†’ true) == false
      #expect(shouldUpdate == false) // βœ…
    }

    @Test
    func When_CallingShouldUpdateWithDifferentId_Then_ItReturnsTrue() async throws {
      let testObject = TestIdentifiable(id: UUID())
      let sut = TestShouldUpdate(currentValue: testObject)

      var newTestObject = TestIdentifiable(id: UUID())
      newTestObject.text = "Changed"

      let saved = sut.save(newTestObject)
      let shouldUpdate = sut.shouldUpdate(newTestObject, replacing: sut.currentValue)

      #expect(saved == true)
      #expect(shouldUpdate == true)
    }
  }
  @Suite("Given_AShouldUpdate_EquatableIdentifiable_Case")
  struct SectionThree {

    struct TestEquatableIdentifiable: Identifiable, Equatable {
      let id: UUID
      var text: String = "Test"
    }

    @Test
    func When_CallingShouldUpdateWithEquatableIdentifiableSameValue_Then_ItReturnsFalse() async throws {
      let testObject = TestEquatableIdentifiable(id: UUID())
      let sut = TestShouldUpdate(currentValue: testObject)

      let saved = sut.save(testObject)
      let shouldUpdate = sut.shouldUpdate(testObject, replacing: sut.currentValue)

      #expect(saved == false)        // πŸ’₯ Expectation failed: (saved β†’ true) == false
      #expect(shouldUpdate == false) // βœ…
    }

    @Test
    func When_CallingShouldUpdateWithEquatableIdentifiableModifiedValueButSameId_Then_ItReturnsTrue() async throws {
      let testObject = TestEquatableIdentifiable(id: UUID())
      let sut = TestShouldUpdate(currentValue: testObject)

      var modifiedTestObjectWithSameId = testObject
      modifiedTestObjectWithSameId.text = "Changed"

      let saved = sut.save(modifiedTestObjectWithSameId)
      let shouldUpdate = sut.shouldUpdate(modifiedTestObjectWithSameId, replacing: sut.currentValue)

      #expect(saved == true)
      #expect(shouldUpdate == true)
    }

    @Test
    func When_CallingShouldUpdateWithEquatableIdentifiableDifferentId_Then_ItReturnsTrue() async throws {
      let testObject = TestEquatableIdentifiable(id: UUID())
      let sut = TestShouldUpdate(currentValue: testObject)

      var newTestObject = TestEquatableIdentifiable(id: UUID())
      newTestObject.text = "Changed"

      let saved = sut.save(newTestObject)
      let shouldUpdate = sut.shouldUpdate(newTestObject, replacing: sut.currentValue)

      #expect(saved == true)
      #expect(shouldUpdate == true)
    }
  }

  @Suite("Given_AShouldUpdate_Any_Case")
  struct SectionFour {

    struct TestNonConformingObject {
      let id: UUID
      var text: String = "Test"
    }

    @Test
    func When_CallingShouldUpdateWithNonConformingObjectSameValue_Then_ItReturnsTrue() async throws {
      let testObject = TestNonConformingObject(id: UUID())
      let sut = TestShouldUpdate(currentValue: testObject)

      let saved = sut.save(testObject)
      let shouldUpdate = sut.shouldUpdate(testObject, replacing: sut.currentValue)

      #expect(saved == true)
      #expect(shouldUpdate == true)
    }
  }

  @Suite("Given_AShouldUpdate_AnyObject_Case")
  struct SectionFive {

    final class TestClassObject {
      let id: UUID
      var text: String

      init(id: UUID, text: String = "Test") {
        self.id = id
        self.text = text
      }
    }

    @Test
    func When_CallingShouldUpdateWithSameReference_Then_ItReturnsFalse() async throws {
      let testObject = TestClassObject(id: UUID())
      let sut = TestShouldUpdate(currentValue: testObject)

      let saved = sut.save(testObject)
      let shouldUpdate = sut.shouldUpdate(testObject, replacing: sut.currentValue)

      #expect(saved == false)        // πŸ’₯ Expectation failed: (saved β†’ true) == false
      #expect(shouldUpdate == false) // βœ…
    }

    @Test
    func When_CallingShouldUpdateWithDifferentReference_Then_ItReturnsTrue() async throws {
      let id = UUID()
      let testObject = TestClassObject(id: id)
      let sut = TestShouldUpdate(currentValue: testObject)

      let anotherObject = TestClassObject(id: id)

      let saved = sut.save(anotherObject)
      let shouldUpdate = sut.shouldUpdate(anotherObject, replacing: sut.currentValue)

      #expect(saved == true)
      #expect(shouldUpdate == true)
    }
  }
}

I also tried another variant of the protocol, but same results apply.

public protocol ShouldUpdateProtocol {
  associatedtype Entity

  func shouldUpdate(_ new: Entity, replacing old: Entity) -> Bool
}

public extension ShouldUpdateProtocol {
  func shouldUpdate(_ new: Entity, replacing old: Entity) -> Bool { true }
}

public extension ShouldUpdateProtocol where Entity: AnyObject {
  func shouldUpdate(_ new: Entity, replacing old: Entity) -> Bool { new !== old }
}

public extension ShouldUpdateProtocol where Entity: Identifiable {
  func shouldUpdate(_ new: Entity, replacing old: Entity) -> Bool { new.id != old.id }
}

public extension ShouldUpdateProtocol where Entity: Equatable {
  func shouldUpdate(_ new: Entity, replacing old: Entity) -> Bool { new != old }
}

public extension ShouldUpdateProtocol where Entity: Equatable & Identifiable {
  func shouldUpdate(_ new: Entity, replacing old: Entity) -> Bool { new != old }
}

Thank you in advance for the help.

Here are some answers that explain Swift's approach to genericsβ€”they address somewhat different questions but all explain the relevant underlying concept (in this case, it answers the question as to why self.shouldUpdate(...) in your implementation of save always calls the same implementation):

1 Like

Thanks for sharing the links.

From the first one quoting you:

The compiler must determine what f means (i.e., which function to call) in the context where it is written.

So this is what I was missing.

I was counting on TestShouldUpdate<Int> at caller site being statically defined concrete type and expecting the compiler to do the right choice but while compiling TestShouldUpdate<Entity> does not have any clues of the callers and can pickup anything other than "no constraints at all".

A little bit hard to digest, but it's ok.

Given so, the only possible way to achieve this is to expose specialisation of TestShouldUpdate for the save function as well.

The following appear to work as expected.

extension TestShouldUpdate where Entity: AnyObject {
  func save(_ new: Entity) -> Bool {
    self.shouldUpdate(new, replacing: self.currentValue)
  }
}

extension TestShouldUpdate where Entity: Identifiable {
  func save(_ new: Entity) -> Bool {
    new.id != self.currentValue.id
  }
}

extension TestShouldUpdate where Entity: Equatable {
  func save(_ new: Entity) -> Bool {
    self.shouldUpdate(new, replacing: self.currentValue)
  }
}

extension TestShouldUpdate where Entity: Equatable & Identifiable {
  func save(_ new: Entity) -> Bool {
    self.shouldUpdate(new, replacing: self.currentValue)
  }
}

Not sure is what I want for my real case scenario, but at least I have more informations to think of.

Thank you.

1 Like