-
Notifications
You must be signed in to change notification settings - Fork 1.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closure PartialEq and Eq #3538
base: master
Are you sure you want to change the base?
Closure PartialEq and Eq #3538
Conversation
Rendered link is broken after second commit. |
Fixed |
Adding an explicit |
Honestly, I feel like the subtlety of having One concern I have is with the syntax, since the The main thing that comes to mind here is that it feels very weird to want to ask if something has changed, by cloning it? For example, if we have an example like: let v = 10;
let closure = move || println!("{v}");
Wrapper(closure) Why not instead make the value parameterised, and then pass it to the closure? let v = 10;
let closure = |v: i32| println!("{v}");
Wrapper(v, closure) Or, if you can't permit this, perhaps the closure can take ownership of a cell or atomic instead? let v = Arc::new(AtomicI32::new(10));
let v2 = v.clone();
let closure = move || println!("{}", v2.load(Ordering::Relaxed));
Wrapper(v, closure) Like, something certainly doesn't feel right to me about the examples provided, like they're just trying to work around a poorly designed API instead of doing the right thing. But they could also just be toy examples and more representative examples could benefit from the proposal. I dunno, it just feels like this has too many caveats to really be useful, and that the ability to implement |
As noted in zulip, you want this declared explicitly both in the trait bounds, as well as when instantiating the closure, so maybe..
If you missed either side, then you'll have counter intuitive behavior. As serde_closure exists, I suppose "procmacro closures" sound promising, by which I mean some procmacros interface which exposes information about the captured variables, etc, so the procmacro code then write Already |
I don't think this is an issue since
The main use case I have in mind for this is React-like UI frameworks, where the user's render function returns a tree of the components they want, and the library compares that tree with the current state of the UI, adding, removing, and moving components based on what the render function returned. The library tries not to burden the render function with the current state of the UI so that the render function can be written in a declarative manner, in which the render function will create new closures every time it's called. The tree diffing algorithm would then need to compare those closures with the current state of the tree in order to know whether to refresh subcomponents or not. Here's a bit more of a concrete example using Yew: #[functional_component]
fn TodoView() -> Html {
let todos = use_state(|| (0..3).map(|i| Todo { id: i }).collect::<Vec<_>>());
html!{
<main>
{for todos.iter().map(|todo| html!{
<SingleTodo
key={todo.id}
todo={todo.clone()}
delete_button_cb={impl PartailEq || {
// Delete code here
}}
/>
})}
</main>
}
} Today, |
I'm not sure if I'm understanding you properly, because I believe this is what my proposal already says, just with a different proposed syntax:
I'm not sure what you mean here. I don't see how closures are special w.r.t. pointer equality, or what
I don't think this is a huge footgun, since it exists in every dynamically typed language with first class functions, and doesn't appear to be a huge issue there. |
Technically saying, functions require checks for full permutations of possible arguments (which grows to infinity quite fast) and all inner states to make a conclusion of Equality and for practical purposes such checking is useless. |
I never claimed existing procmacros should be used for this feature. I suggested that "procmacro closures" would make rustc expose information about the captured variables, which then makes better ergonomics possible in
Afaik Anyways.. At a high level, this feels niche & footgun prone. If "procmacro closures" could provide this, then (a) fewer people use this, which overall reduces footgun exposure, while simultaniously (b) more projects benefit from closure syntax by doing whatever traits their niche requires, including core ones like |
I see - you mean a new form of procedural macros that expose the underlying closure fields in a way that the proc macro can see into. Is there an RFC for that?
If I'm understanding this right, the concern is that it's hard to tell what variables are being closed over and their types, so the types could change (ex.
And this is a concern that a closure closing over, say, a
I'm not going to disagree that its niche. Procmacro closures indeed sounds like a more flexible alternative. I brought up this RFC because from what I understand, it should be fairly easy to implement via copying what is already done for |
I understand the motivation of this feature for virtual DOM frameworks, but the syntax seems very verbose for that use case. Already |
Perhaps a |
This makes the exact details of what's being captured -- particularly fields vs full structs -- matter even more than it did before. At least with As such, I remain skeptical of more traits for anything with implicit captures. With an explicit exhaustive list of captures, though, things might be be different -- the fragility of behaviour to seemingly-irrelevant changes would turn into "no, you didn't capture that" errors instead. That could also give an order to be able to extend this to |
So, two things I don't see spelled out at least on the GitHub side of things. In languages like JS one is getting reference equality, but in Rust this would be value equality. First, it is very easy to capture (say) a large Vec for one mostly inconsequential operation but the closure must compare all fields rather than some explicit subsets of captures. While one can assume that this won't happen if fully in control of both sides, an API which invisibly relies on this could easily capture enormous data structures in contexts where it is assumed that comparisons are reasonably cheap, thus defeating the purpose. This would be basically impossible to debug, not only because it's hard to spot but because it is often the case that you end up with collections that mostly compare unequal until suddenly it has to do the first 99% to find the element at the end because of some pathological path. Secondly, in JS at least, evaluating the expression Notably, React's state abstractions will re-render if the closure is changed even if the new closure wouldn't do something different; today's status quo in Rust is at least as good, in the sense that a library should be able to offer closures-but-reference-equality to users that is at least as good as JS. I honestly see the no explicit list of captures problem as large enough that the explicit syntax here seems like a waste, if nothing else; it's nice, but it's not going to really lead to better error messages, and it's not going to guard against my first concern around accidentally expensive impls. Public API surface area primarily occurs at function parameter and return value boundaries, and that is expressed in the constraints on type parameters. I don't see why an error message along the lines of "this closure should be Eq because it's used here and this capture isn't" doesn't solve the problem, modulo implementation difficulty which I can't comment on. The syntax is currently an annoying middle ground where it's not explicit enough to be useful, and it's not required enough on the face of it to give good error messages or constrain public APIs that we wouldn't get anyway. |
I certainly could undo the changes I made to add in the explicit Though I'm not sure if I should bother. I'm not sure what the next steps are for RFC proposals after posting them and it seems like this one is just sitting here with not really great reception. |
The team has to look at it essentially. They have limited bandwidth and this isn't solving a huge ecosystem-wide problem. E.g. it's not Tait for instance. Not every good RFC gets a quick response, not that I've seen. I'd not take this as a sign of their opinion on your own especially not this close to the holidays. My reception is neutral because while I agree that this solves a problem for some definition of solves and some definition of problem, it's also entirely possible to port React hooks to Rust and also to get JS-style reference semantics on closure comparisons without language help. I'm not sure if anyone has written crates for either, but I have given both thought in the past as well as ways to place dyn Fn optionally inside a SmallVec equivalent. I'm just more concerned about the problems it introduces. Otherwise yes, neat solution to a somewhat rare but still common enough use case. I ask myself questions like "if we were doing a code review, how could I spot problems before production?" and here I cannot, when that problem is either "comparing things that don't make sense" or "comparing things that can be hugely expensive to compare". Consequently, on a team, this feature would--by whatever amount--reduce the reliability of the software the team produces. It is hard to quantify by how much in practice without widespread usage to draw conclusions from, but certainly in theory. Also has anyone brought up the point that this can unintentionally cause one to rely on Eq impls from libraries, which may or may not change what they do? That's not necessarily a problem because it seems it'd be rare to have a library introduce a bug in PartialEq, but performance regressions seem like they'd be a reasonable thing to see happen and--as with the large containers style bug--this makes it super easy to just eagerly use every PartialEq impl on the planet. |
So, here is a problem. Rust supports splitting borrows when using closures. See this: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=74e660a2bb07b2223ae831eba03cb828 In that example, Split doesn't have a derive, but c1 and c2 are still, technically, comparable. If there were a syntax for explicit captures, it'd need to handle this case; if not, what should the behavior here be? I don't think that letting a closure be comparable because it happens only to capture fields of a non-comparable type is very sensible, but doing anything else widens the impact of this change by turning off splitting these borrows. |
It shows the splitting better if you use mutable borrows like https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=95984003646c3144ac94f5ca3d201a35 If we As I said above, rustc could expose the anonymous type and implicit caputres to proc macros, which simplifies proc macros for this, serde_closure, and other traits. |
I think it would be possible to write a proc macro to find the captured identifiers of a closure, though not split fields, and make that generic. I think that would let me write generic closures with the serde_closure trick so I've been thinking about it but it's not a small undertaking. Good call on the mutable borrows. Didn't spot that myself. Something else I thought of: it's not strictly guaranteed that comparison traits are pure. That is, There's also the case of mutable statics. Both this and the above point may mean that for any closure Neither of those are condemnations from the language perspective--it's entirely possible to define the semantics and say you deserve what you get if you're evil--but in my view they do decrease the motivation. Reference equality avoids these pitfalls. But here's a good one: if a closure is equal to itself and then mutates something it captures, what should libraries trying to use this feature do? For example just incrementing a counter. Saying "well Fn" isn't enough; atomics exist and are commonly used for such. This RFC conflates Eq as in data with Eq as in execution, and is trying (or at least will be perceived as trying) to say that if two closures are Eq then they do the same thing. I see a few pieces here:
If you have explicit captures (thus avoiding the pitfalls around just what's included) and the requirement of purity, I think this gets somewhere; in that case you can know if two closures do the same thing. Otherwise, this is the much weaker guarantee that some bytes happen to be the same right now, effectively; in many ways that's even less useful than the reference equality of JS, since the thing involved in the comparison can mutate its own representation. Finally I'll throw out that I'm pretty sure people want generic closures. I certainly do at any rate and I'm pretty sure that's had a lot of discussion over the years. This should probably consider that future as well. The more I think about this RFC the less I like it in this form but the more I think of it as interesting. Useful, I'm not sure, but there's certainly good ideas here. It's just all the corners that nibble at it until there's enough problems that it falls over. |
I doubt explicit captures serve much purpose, likely
We discussed this above, presumably serde_closure does this, and breaks on split borrows, but anyways this sounds nasty & unmaintainable. Ideally, rustc could tell proc macros how it writes a given closure, and give the proc macro field access, so then derive code looks relatively normal. It's fine if humans cannot write this code, specify captures, etc. Also, there are many traits which almost resemble closures, but cannot be closures for technical reasons, like they provide default methods that a few types override. |
If rustc tells proc macros how it writes a given closure, then that means running macros after that point. That would likely be a new kind of macro, would it not? Seems equivalent to solving the general problem wherein figuring out properties of types is entirely left to the ecosystem, which is fragile in way more ways than just this problem in any case. In this case, I'd imagine split borrows are computed only after the borrow checker. So, maybe the macro spits out code but presumably that'd then need to backtrack and run the borrow checker again. Opening this up to custom derives also makes the representation of closures fixed. So for example no raw pointers or compiler-generated undefined behavior but this is the compiler optimization or anything. In particular with the comparison traits, and with closures as they are today, I don't think anything stops the closure representation from being a single pointer to the current stack frame from which captures are taken under the condition that the closure only borrows from that frame. I don't think Rust does but it could if it wanted. Derives of built-in traits could still optimize some traits of course but the general case suddenly makes closures bigger/slower in the theoretical world where rustc got clever. I'm pretty sure that llvm couldn't do this kind of optimization because llvm doesn't do type layout changes and a generated struct with n fields always has n fields. Such things very much seem in rustc's wheelhouse unless someone knows things I don't. Also this again circles back to generic closures. With derives as they are today, it is unlikely Rust will add a new kind of core type on which one may wish to use a custom derive, but generic closures would be a significant new closure syntax that would almost certainly break such derives. Are generic closures dead at this point? If so that's a shame, because I could really use them to port some C tricks that I'm having to use proc macros for. |
I think that's what @burdges was saying above - adding a new type of proc macro for closures that gets the info about the closed-over values. Though, I'm not sure how possible that is, especially with borrowing struct fields - the compiler would have to parse and typecheck enough to know that a field access is a direct access and not though a |
Yes, rustc would run the proc macro after rustc generates the closure. It'd only add code though, so I doubt this requires rustc "backtrack", merely that earlier passes run over the added code. I'd expect this imposes restrictions upon generated code of course.
Interesting, we seemingly do not care about the representation of the closure itself, but merely what values the closure provides. Again not impossible, but I think your point is: This is not easy. Alright fair, thanks. |
Well, the thing is that nothing stops the macro from being evil and spitting out code that tries to access the closure's captures. Modifying the closure is also a problem but nothing prevents doing something like:
What should one do if this is after type/borrow checking, and But really the bigger problem is that running code this late is running after type inference. You can't just plop new code in because it can change type inference all the way up to the boundary established by the fn item, and even beyond if the fn is using impl trait (because the return type would change after this rewrite). Indeed it is quite possible that the "first pass" of type checking wouldn't be able to complete without the expansion, since the derived trait is presumably required. But the boundary here is potentially the entire crate graph if the user returns a closure. I suspect macros like this are possible, but only if it's something more like "generates new files" and even then it wouldn't be easy and still has lots of these problems since that file could implement a trait, thus causing the same chaos. But if one is generating entirely new files rather than being able to modify the local scope, it's not possible to get access to the closure in a way which would allow for this. I'm pretty sure that I could invent a counterexample for any simple enough to be useful version of custom derives on closures which would require rebuilding significant amounts of code, even without being up to date on compiler internals. |
Pre-RFC Discussion: https://rust-lang.zulipchat.com/#narrow/stream/213817-t-lang/topic/PartialEq.2FEq.20for.20Closures.3F/near/399369137
Rendered