How does the compiler handle `Equatable` in this case?

While reading the internal implementation of this library I spotted a strange implementation for Equality.

Here is a simplified version.

// In module A
public struct Test {
  public let a: Int
  public let b: String
  public init(a: Int, b: String) {
    self.a = a
    self.b = b
  }
}

extension Test : Equatable {}

public func == (lhs: Test, rhs: Test) -> Bool {
  return lhs.a == rhs.a
}

// In module B
import A

let t_1 = Test(a: 0, b: "Hello")
let t_2 = Test(a: 0, b: "World")

t_1 == t_2 // true 
t_1 != t_2 // false
t_1 != t_1 // false

That makes me wonder if the old way of making types conform to Equatable protocol is still permitted in the Swift 4.2 world or did we considered the move from global to static type functions a breaking change back then?!

In case of the former it would mean that the compiler thinks that we explicitly provided an implementation for the static func == (lhs: Test, rhs: Test) -> Bool method right?

Maybe I’m missing something here, but what exactly is unexpected? I would hope the compiler could tell we’re providing an implementation of Equatable, otherwise I don’t understand how it would have been source compatible.

The way the conformance is expressed / satisfied is a litte mind bending, in this particular case.

Not all major Swift releases were source compatible and we had a ton of accepted breaking changes over the past years. In the early iterations we had code that looked like this:

extension Test : Equatable {}

// Operator functions were global functions
public func == (lhs: Test, rhs: Test) -> Bool

Then it was changed to this:

extension Test : Equatable {
  // Operator functions changed to static type methods
  public static func == (lhs: Test, rhs: Test) -> Bool
}

Then we had SE-0185 which brought automatic implementation synthesization and would simplify everything to:

extension Test : Equatable {}

If you want to override the automatic behavior it's as simple as writing the function yourself:

extension Test : Equatable {
  public static func == (lhs: Test, rhs: Test) -> Bool
}

However the example in the OP is written the very old school way which is irritating. In my simple example it seems that the compiler is doing the right job, but I really can't tell for sure.


In other words: There is a custom == implementation written outside the scope of the type (extension), which seems to prevent the automatic synthetization.

Is this behavior in this particular case correct?

My current knowledge is that automatic synthetization is applied to declarations that are missing in the source file. The old (free function) way to implement == has not been deprecated. If declared in the same file as the Equatable conformance, it prevents synthetization.

If my hypothesis is true, defining == in another file should yield a compiler error (duplicated declaration).

Well that's always true for any global function in the same module, however in Playground if the code from OP is in a source file you can redeclare a global == function in the main playground page, which will completely override the behavior in the local scope. (Not sure if Playground treats pages as seperate modules or something along the lines.)

Maybe this free function way should be finally deprecated?

Well that's always true

I thought you were asking a question.

however in Playground

The context was a library.

Maybe this free function way should be finally deprecated?

What problem are you trying to solve?

I'm just trying to understand the way the compiler treats the code written that way with todays version of Swift.

Sure, but then it's just the same for modules. If the original code written that way is in module A then you can probably create a custom global function in module B that will shadow Test.==. That was basically the observation I mentioned in my previous post using Playground.

I'm wondering if it's still necessary to drag this functionally around or if anyone still relies on this.

Okay some new observation which is even more confusing. I'm using explicitly Playground this time.

// Source_file_1.swift
public struct Test {
  public let a: Int
  public let b: String
  public init(a: Int, b: String) {
    self.a = a
    self.b = b
  }
}

extension Test : Equatable {}

// Source_file_2.swift
public func == (lhs: Test, rhs: Test) -> Bool {
  print("🔥")
  return false
}

// Main page
let t_1 = Test(a: 0, b: "Hello")
let t_2 = Test(a: 0, b: "World")

t_1 == t_2 // false - prints "🔥"
t_1 != t_2 // true - prints "🔥"
t_1 == t_1 // false - prints "🔥"
  • Why is this valid?
  • Now it feels like the compiler provides a synthetization for Equatable, but the global free == function shadows everything while even making != work.

As an exception, the compiler treats global operator functions as witnesses for matching protocol requirements when it isn't able to find a regular viable witness. Naturally, lookup is always prioritized over synthesis, so in your case the compiler satisfies the conformance by discovering == through unqualified lookup and nothing is synthesized.

1 Like
Terms of Service

Privacy Policy

Cookie Policy