Comparing closures

Is there a way to compare references to 2 closures to determine if they are the same?

Nope.
You can invent your solution for this depending on the use case.
E.g. you can wrap the closure in a class, so you can use === comparison.
Or wrap the closure in a struct with let id = UUID().
Then you can implement custom Equatable conformance.
You can also introduce callAsFunction for the convenience of the wrapper type.

1 Like

Thanks for the idea, but this didn't seem like what I wanted (wrapping the closure).

ChatGPT gave me some ideas and I came up with this solution:
Ussing withUnsafePointer to get the address of the closures and then comparing them.
This seems to work fine, and seems more reasonable than a wrapper for what I want.

No. Function values do not have a stable identity in Swift. The compiler may merge different functions that end up with identical machine implementations, leading to different functions having the same entry point address, and conversely, a function may end up being thunked any time its calling convention needs to be changed, and these thunks are not uniqued, leading to different uses of the same function or closure literal potentially having different addresses. When or if these sorts of transformations occur is arbitrary and can change between compiler versions, if you use different compiler settings, whether you use static or dynamic libraries, etc., so you cannot rely on casting to a pointer giving the same result in all circumstances even if it appears to work in isolated examples.

You might be able to use a KeyPath which does have an == operator if you need to refer to declarations with stable identities.

28 Likes

Joe's observation means that there is currently no way to determine precise equality for closures, nor is there likely to be such a way anytime in the near future. In particular, ChatGPT's suggestion is not going to be correct in all cases.

However, precise equality is not necessary for all use cases. Another approach is to have a comparison that can answer "Yes, they're equal" or "I don't know." This would allow you to avoid some work when something has definitely not changed, as long as you are tolerant of sometimes doing unnecessary work. The hardest problem: I'm not really sure what to call such a comparison. :person_shrugging: If we could come up with a good way to talk about such a comparison, that would make it easier to understand when and how it would be useful, which is the first step towards trying to get wider acceptance of it, possibly even building it into the language some day as a standard feature.

In the case of closures, this would still be non-deterministic, for example moving code between files or enabling/disabling optimizations might change a "yes they're equal" result into "I don't know" or vice versa, so I'd be very hesitant to introduce it as a standard feature.

I think here, the best solution might be an enum with a synthesized Equatable conformance. The function pointer becomes the enum case, and the captures become the payload. Now, == will behave as you expect, given two instances of such an enum:

enum Operation: Equatable {
  case formatDisk(volumeName: String, drive: Int)
  case eject(drive: Int)
  case duplicate(srcDrive: Int, dstDrive: Int)

  func perform() {
    switch self {
      case .formatDisk(let volumeName, let drive): ...
      ...
    }
  }
}
6 Likes

If your specific use case is passing a function to another function that takes a closure as a parameter:

func global() { }

func f(local: () -> ()) {
  //  TODO: assert(local === global)
}

You might be able to perform this test in C (where function symbol pointers are legit for comparing for equality by identity). AFAIK you also have this option with ObjC methods (using selectors and imps). I'm not a specialist in C++ to tell you whether or not that also works (maybe it does?).

If your types bridge cleanly into one of the C languages… you might try wrapping that comparison test in a small library module (written in C) that performs the identity equality check against the function pointers (and then returns the result back to your Swift module).

I believe this behavior is not allowed by the C standard, for similar reasons to Joe's post above.

Hmm… do you have any resources where I could learn more about this behavior? Would this at all be related to D141310 and the icf optimzations that are passed to llvm?

Many people wish this was the case, but unfortunately C requires pointer equality on function pointers to work (the same function must compare equal, and different functions must be non-equal), which gets in the way of a bunch of potential codegen optimizations. Notably ICF is forced to be an opt-in thing, and inline functions are worse than they could be. lld's --icf=safe attempts to only merge functions which do not have their address taken, but I believe that's broken in some situations.

6 Likes

I'm not sure if this entirely applies to function pointers that are created from Swift, since they could be pointing to a local thunk. At best you'd get a stable address from each place that passes a function pointer to C, but no guarantee that other passed pointers to the same function have the same address.

There’s also the limitation that @convention(c) closures can’t capture any state (since they are just syntactic sugar for declaring an anonymous global function), unlike normal closures and blocks which each have a context pointer next to the code pointer.

2 Likes

C's rules do not apply to Swift functions, and Swift does not guarantee any consistent identity for Swift entry points or closure values. C function pointers to functions defined in C will have a consistent identity following C's rules.

5 Likes