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
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.