How does Equatable synthesis interact with implementing refined protocols?

AFAIK, to get any of the synthesize-able protocols (Equatable, Hashable, Encodable, Decodable) to be synthesized, you generally have to declare that protocol in the type's primary definition.

What about refined protocols that are not synthesized themselves? Specifically, what about Standard Library protocols refined from Equatable (besides Hashable)? If you declare something to be Strideable, possibly in a separate extension block, will that protocol's default implementation of == be used? Or will the one synthesized in the primary definition block be kept? It's important for Strideable since it sometimes may have recursive definitions.

If it makes a difference if Strideable is declared in the primary definition, an extension in the same file as the primary definition, or a separate file, let us know.

There's always[1] only one protocol conformance for a type to a protocol, and one set of implementations of the requirements. Those implementations will come from synthesis only if there's no other implementation that works, including default ones from protocols, e.g.

protocol P: Equatable {}
extension P {
    static func ==(lhs: Self, rhs: Self) -> Bool {
        print("using default")
        return true
    }
}
struct X {
    var property: Int
}

extension X: P {}

print(X(property: 1) == X(property: 2))
using default
true

This shouldn't be affected by where the conformances are declared, except to stop synthesis from happening at all (it can only occur in the same file as the type's definition).

Are you encountering something unexpected in your code?

[1]: One can have multiple conformances if they're declared retroactively in different modules, e.g. if two modules write extension Int: Collection, but this doesn't affect this case.

No, I haven't done it yet. I'm wondering if adding Strideable conformance via an extension will mess up the automatic Equatable conformance in the primary definition. This is important for Strideable because its default implementation for == can have infinite recursion in some circumstances.

To reuse your example:

protocol P: Equatable {}
extension P {
    static func ==(lhs: Self, rhs: Self) -> Bool {
        print("using default")
        return true
    }
}

//...

struct X: Hashable, Codable {  // Critical difference: addition of automatic Equatable conformance
    var property: Int
}

extension X: P {}

// Is the automatically-defined version of == from the primary definition used?
// Or does the default implementation from P get used instead?
print(X(property: 1) == X(property: 2))

Yes, it will. Running your adjusted example also prints:

using default
true

In my case, that's exactly what I don't want. I want what the automatically-defined == would be without Strideable (or any other protocol importing a default implementation of ==) changing it. How would I get it back? Just do the default comparisons in a manually defined method?

Wait, what happens if more than one protocol brings in a default implementation?

That's an interesting point and potentially counterintuitive. Conforming to two protocols with default implementations would ordinarily require the concrete type to implement the method; in this case, you do get synthesis back because of that.

Therefore, you could work around your issue by creating an internal protocol refining Equatable named _RestoreSynthesizedEquatableConformance with a default implementation of ==:

protocol _RestoreSynthesizedEquatableConformance : Equatable { }

extension _RestoreSynthesizedEquatableConformance {
  static func == (lhs: Self, rhs: Self) -> Bool { fatalError() }
}

struct S { }

extension S : Strideable, _RestoreSynthesizedEquatableConformance {
  func advanced(by n: Int) -> S { return S() }
  func distance(to other: S) -> Int { return 0 }
}

S() == S() // true
1 Like

Wouldn't the primary definition line need to be:

struct S: Equatable {}

or is my understanding of the rough corners worse than I thought?

My strong type-alias proposal ideas generally had some way to bring back members of the underlying type. This included ways to bring back an entire protocol's worth of members at a time:

typecopy MyType: Int, Comparable {
    // I could use "publish Comparable" but I can satisfy the protocol in pieces too:
    publish Equatable
    static func < (lhs: MyType, rhs: MyType) -> Bool { /*my custom implementation...*/ }

    // Add at least one initializer....
}

Maybe we should have insisted on something like this for the automatically-defined protocols, instead of the definitions being implicit on protocol declaration.

In fact, if we do add strong type-aliases, and with a publish feature, can we insist on it for Equatable/etc.? Make it optional for the first version of Swift with strong type-aliases, the preferred way on the next major version (i.e. make the current way depreciated), then the only way on the next major version after that.

Nope, that's no longer required because same-file conformance can now be synthesized.

That was a main point of discussion during review, and it was decided after taking into consideration opposing viewpoints that implicit was the way to go, so that is a settled question.