Observation with Equatable

Hi, I'm curious why Observation doesn't do an equivalence check on the Equatable type.

One of the pain points about Published is that when the value being set doesn't change, it results in a lot of invalid updates attempts, will Observation optimize for the Equatable?

Thanks!

3 Likes

I think there would be another problem if Observation optimises for the Equatable.

struct Foo: Equatable {
    var a: Int = 0
    var b: Int = 0

    static func == (lhs: Foo, rhs: Foo) -> Bool {
        lhs.a == rhs.a
    }
}

@Observable class Bar {
    var foo = Foo()
}

When foo.b is modified, the update won't be triggered.

what if you add @Observable for Foo? and you access Foo.b in the register block, the update will be triggered.

Because the macro has no knowledge about the type (this is a limitation of macros in general) the only way to determine the equitability is to hold the previous value and at runtime check if the type is equatable (via some really ugly hacks) and compare it (with even more ugly hacks). This opening routine is rather costly. From measurements this was not worth it; effectively it erased the gains of just observing the things that were used.

This has much more impact for the production of specific property observation; from the Combine perspective that is the subscription to the publisher of the @Published. That can, if done carefully, be done at compile time without those costs.

1 Like

Given how @Observable works, the update will be triggered, but the theoretical == check triggered before exposing the new value would return false for b, meaning the update would always happen regardless of whether deduplication was active. In that example only a would be deduplicated. This could potentially point to the need for a separate protocol that allows this sort of observable deduplication without having to customize a type's Equatable conformance, but it's unclear whether that's really necessary yet.

The Check can be added without runtime cost, like the Codable.

By changing withMutation to this, complier can select the property version when the type is Equatable

@Observable
class ObsClass {
    @ObservationTracked
    var int = 0
    {
        @storageRestrictions(initializes: _int )
        init(initialValue) {
            _int  = initialValue
        }

        get {
            access(keyPath: \.int)
            return _int
        }

        set {
            withMutation(keyPath: \.int, storageKeyPath: \._int, newValue: newValue)
        }
    }

    internal nonisolated func withMutation<Member>(
        keyPath: KeyPath<ObsClass , Member>,
        storageKeyPath: ReferenceWritableKeyPath<ObsClass , Member>,
        newValue: Member
    ) {
        _$observationRegistrar.withMutation(of: self, keyPath: keyPath) {
            self[keyPath: storageKeyPath] = newValue
        }
    }
    
    internal nonisolated func withMutation<Member: Equatable>(
        keyPath: KeyPath<ObsClass , Member>,
        storageKeyPath: ReferenceWritableKeyPath<ObsClass , Member>,
        newValue: Member
    ) {
        guard self[keyPath: storageKeyPath] != newValue else {
            return
        }
        _$observationRegistrar.withMutation(of: self, keyPath: keyPath) {
            self[keyPath: storageKeyPath] = newValue
        }
    }

    @ObservationIgnored private var _int  = 0
}
extension ObsClass: Observation.Observable {
}

Or if swift needs to change the withMutation to this form, then developers can add their own variants withMutation for Equatable, Hashable, Identifiable....

Yes, based on the current Observable, updates will be triggered anyway. However, if we optimize it by performing equality checks on Equatable types, then when foo.b changes, the update will not be triggered.

That's not a bad approach, it definitely resolves the problem from what I can tell without the hacks that would be needed else wise. It would need to be explored on what the ramifications of changing the emissions would be - well worth the investigation.

If you end up opening up a PR to change the macro code generation, tag me on it and we can iterate through a potential path to getting this fixed.

Will do! Thanks very much!

PR Unleashing the Potential of Observation by miku1958 · Pull Request #68107 · apple/swift · GitHub

1 Like