Add a mechanism and syntax to implement PartialEq and Eq for closures:
// Simple example:
+ Eq + Fn
Motivation
In some situations, there is a need to compare closures to see if the values they have closed over are different.
For example, React-like UI frameworks optimize by only re-rendering UI elements if their parameters have changed, which necessitates that their parameters implement PartialEq. They also need to support passing in functions within these properties - for example, a callback to call when a button is clicked.
Because closures don't implement PartialEq, UI frameworks have to use workarounds to support callbacks, for example, by bundling a function with a PartialEq-implementing "state" structure that is passed along side the normal arguments when the callback is called. This involves a lot of boilerplate, and makes users unable to use the conveniences involving the Fn, FnMut, and FnOnce items, since they can't use those traits.
This RFC solves this issue by allowing closures to opt into implementing the PartialEq and Eq traits, similar to how closures can currently implement Clone and Copy.
Guide-level explanation
By prefixing a closure with impl PartialEq or impl PartialEq + Eq, closures can opt into implementing the PartialEq and Eq traits. All values of the closure must implement PartialEq or Eq.
// `x` is `PartialEq + Eq`, so the closure returned by `gen_print_int` is `PartialEq + Eq`:
+ Fn
Noe that this does not change the fact that closures are anonymous types. Therefore, only closures originating from the same code will have the same type and will be comparable.
The implementation of PartialEq and Eq is opt in, to avoid issues where closures may unintentionally gain or lose PartialEq or Eq in an API breaking way.
Reference-level explanation
Alter closure parsing to accept an optional prefx of impl [trait-bounds].
If the optional prefix is specified and the trait bounds is PartialEq, the closures's closed over values must also implement PartialEq, and the closure will implement PartialEq<Self> by comparing all of its closed over values.
If the optional prefix is specified and the trait bounds is Eq, PartialEq + Eq, or Eq + PartialEq: The closures's closed over values must also implement Eq; the closure will implement PartialEq<Self> by comparing all of its closed over values (as with impl PartialEq); and the closure will additionally implement Eq.
If the prefix is not specified, closures are parsed and generated as normal.
Implementation should be similar to how closures currently implement Clone and Copy automatically.
Drawbacks
Similar to Clone and Copy on closures, users will have to reason about which variables are being captured to understand whether or not a closure is PartialEq or Eq. The explicit syntax helps highlight that PartailEq or Eq is necessary.
Because closures are anonymous types, the same closure code in two different places will not be comparable, since their types differ. This may be confusing for users who expect two closures that look the same to be comparable. However, this problem already exists with assigning closures to variables and fields.
This may be confusing for types with shared ownership and interior mutability, as closures may compare equal but update unrelated objects. However this issue exists on its own when comparing the objects outside of closures as well.
If a guarentee is made that closures must use all closed over variables in their PartialEq implmenetation, it may forbid optimizing out the variables.
Rationale and alternatives
This RFC proposes an opt-in syntax to PartialEq and Eq, based on feedback and concerns about closures gaining or losing PartialEq or Eq unexpectedly and the resulting API breakage. This is currently in contrast to how Clone and Copy are currently automatically implemented in applicable closures. Doing the same automatic implementation should be possible for PartialEq and Eq, if desired.
Prior art
Most languages with first-class functions implement equality comparison, but only check if two closures are the same object, rather than comparing closed-over variables. Examples of such are Python, Lua, and JavaScript.
Some projects pass in a combination of a function and state, passing a reference to state into the closure when it is called and using PartialEq on the state object. This is similar to how closures are already implemented under the hood. For example, the Yew UI framework has a Deps generic on many of its hooks that accept closures for the state object.
The serde_closure crate implements a procedural macro that implements Clone, PartialEq, serde traits, and a few others on closures by scanning a closure body for references to undeclared variables, then storing them in a structure that represents the closure - essentially, a manual implementaiton of closures.
Unresolved questions
- Should this be op-in, or automatic like
CloneandCopyare? If not, is there any concern of the[Partial]Eq-ness of a closure being part of a public API?- In particular, how would API breakage happen? Closures can only be passed/returned via
implanddynsyntaxes, wherePartialEqmust be added for the PartialEq-ness of the closure to be visible.
- In particular, how would API breakage happen? Closures can only be passed/returned via
- In discussion about this RFC, there was some commentary about the automatic implementations of
CloneandCopynot being desired. We could potentially address this in a backwards compatible way where if theimpl ... ||syntax is used, the closure will not automatically implementCloneandCopyand must opt into them manually. Should this happen, as part of this RFC or otherwise? - What guarentees does/should the compiler make regarding optimizing away closed over variables and how those would be used in the closure's implementation of
PartialEq/Eq? How would this also apply for generators and coroutines?- What guarentees do closures currently have about
Clone? It's likely that everything is cloned.
- What guarentees do closures currently have about
- Concerns about function code merging affecting equality. Could a
dynreference to a closure share a vtable with another closure, and be downcasted to another closure type viaAny?- Does this happen today with closures that implement
Any? I.e. is there a case where a closure defined in two different places shareTypeIDs? - Does it matter, considering they will have the same code? For the React-like UI case, it shouldn't, but there may be other use cases where it does.
- We currently allow
PartialEqbetweenfnpointers, which can also be merged.
- Does this happen today with closures that implement
Future possibilities
The opt-in syntax can be extended to other traits as well - for example, Debug.
The syntax could also be extended to be used with async [move] {} blocks and generators, where it may be desireable for them to implement PartialEq, Eq, Clone, or other traits.