-
Notifications
You must be signed in to change notification settings - Fork 543
Add React #191
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
Add React #191
Conversation
let mut reactor = react::Reactor::new(); | ||
let mut input = reactor.create_input(1); | ||
assert_eq!(input.value(), 1); | ||
input.set_value(2); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From what I read of https://www.reddit.com/r/rust/comments/2uvfic/why_doesnt_rust_have_properties/, this is slightly un-Rusty, but one point made is "Don't add ways to access/modify data unless it's absolutely necessary." and I believe in this case it is necessary since when a value is set the cell needs to propagate the value to any dependents. This isn't possible if the interface is a simple input.value = 2
I'm going to need to back off from this until I understand lifetimes better, it seems! Maybe I'll implement a different problem in the meantime. |
Most of the intermediate commits are useless so I'm going to squash a few of them. The only interesting ones are the ones with my failed attempts, so that reviewers have an idea of what didn't work. And I fully expect that everything will get squashed when merging. |
because the value returned by compute only lives for the stack frame, I believe.
This reverts commit 4d95619.
Lifetime, because ComputeCell needs to live at least as long a the Reactor storing it. I don't think this is an insurmountable problem, but I'm not sure where to put the lifetime annotations now
This reverts commit 13886af.
NOPE, then you can't return anything from a compute cell
This reverts commit 88f2463.
I have completed the example implementation (which will probably be the implementation I submit to the site) and the stub so I am removing WIP and saying it's ready for review. |
// Return an Err (and you can change the error type) if the cell does not exist. | ||
// | ||
// If the callback does not exist (or has already been removed), it is up to you whether to | ||
// return Ok(()) or an error, as it cannot be tested. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not true. repeatedly calling remove on the same callback ID can test this. Only question is whether we should:
- Test it and expect
Ok(())
- Test it and expect
is_err()
- Test it but don't expect any particular value
- Not test it.
In any case, change the comment.
Open question about example implementation and the type system: Is it possible to make it so that the callbacks' borrows end when they are removed, which would be earlier than when the reactor goes out of scope? In general I'm not so sure this is possible in the type system, since the type system has no way of knowing what the |
dummy: T, | ||
} | ||
|
||
// You are guaranteed that Reactor will only be tested against types that are Copy + PartialEq. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
open question: Can the requirement of Copy be removed? It was very difficult for me to see how.
Thinking in terms of what would have to change in the example impl. I don't have rustc to help me verify any of this right now though. I'll verify in a few hours.
If T is not Copy
and create_input
is made to pass an &T
:
- then the best
Cell
can do is to also store the&T
- But then, if the compute functions return
T
, how will that be dealt with? Perhaps https://doc.rust-lang.org/std/borrow/enum.Cow.html ? But that meansT
has to beToOwned
. Still, maybe that's better - On the other hand if the compute functions return
&T
... but how is that possible? Where can they store aT
that will live long enough? I don't think that's possible.
If T is not Copy
and create_input
still passes a T
:
- I think the cell can take ownership for
value
, and a reference forlast_value
. - I don't understand how that can work - when
value
gets replaced, what keepslast_value
alive? - I guess I'll see what happens when I try it.
- Callbacks and compute functions have to take &T, that should be no problem.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At the very least, Clone is more general than Copy (https://doc.rust-lang.org/std/clone/trait.Clone.html, and see how you must be Clone to Copy: https://doc.rust-lang.org/std/marker/trait.Copy.html), so perhaps the Copy can be relaxed to Clone?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not necessarily what we want, because clone might be expensive and copy not, as https://doc.rust-lang.org/std/clone/trait.Clone.html describes. So maybe Copy is what I want.
The difference is currently academic. We only test against usize right now. I would like to not worry about this too much.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If T is not
Copy
andcreate_input
still passes aT
:
- I think the cell can take ownership for
value
, and a reference forlast_value
.
Illegal.
src/lib.rs:23:26: 23:33 error: `initial` does not live long enough
src/lib.rs:23 last_value: &initial,
^~~~~~~
src/lib.rs:21:60: 30:6 note: reference must be valid for the lifetime 'a as defined on the block at 21:59...
src/lib.rs:21 fn new(initial: T, cell_type: CellType<'a, T>) -> Self {
^
src/lib.rs:21:60: 30:6 note: ...but borrowed value is only valid for the scope of function body at 21:59
src/lib.rs:21 fn new(initial: T, cell_type: CellType<'a, T>) -> Self {
Of course. Since initial
is the parameter to that function. Doh.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK in that case I don't care enough. I'm leaving it as Copy
.
A lot going on in here. Interesting. I probably won't be able to dig into this code for a few days, but my glance at the public API seemed ok. |
get and get_mut had too many problems: once you set_value on a cell, how does it know to propagate its changes to its dependents? It either needs references to its dependents or its reactor.
This will make the exercise harder. Students will have to figure out that they need to annotate the Fn and FnMut types. (Or maybe they'll be more clever than I was...)
I had a question about "hey, Reactor is generic over |
What is left on your to-do list for this one, @petertseng? |
I have no implementation-related TODOs, so let me review the questions I've asked to see whether they've gotten answered. |
Edited questions of interest into the PR description. Summary:
If the answer to all these questions is "what's in this PR is OK", then I'd be inclined to merge. |
I think this is fine.
Since we're saying that the inputs are always going to be primitives I think Copy is fine.
I'm not sure what this means. The kinda-weird extra scope in some of the tests?
Well, we have tests that say we can remove callbacks multiple times, so
I'm good with generic. You've already positioned this problem at the end of the Rust track (which is appropriate), so generics won't be new. |
Correct. Currently, I don't see a way to avoid it, so I'm going to leave it.
What I hear from this comment is "it's inconsistent that one of these is Okay, in that case should the API change to this: // If both the specified callback and the specified cell exist, removes the callback and returns true.
// Otherwise, returns false.
pub fn remove_callback(&mut self, cell: CellID, callback: CallbackID) -> bool {
unimplemented!()
} This seems to make sense to me. |
Oh, my opinion's not that strong. Maybe there's a good reason to return an I'm fine with either approach. |
Hmm. HashSet#remove returns just a (Aside: We could imagine an API in which you would only need to pass in the Callback ID to Anyway, so I think I'll say it should be an error to remove a nonexistent callback (including if it was already removed). Why did I think it would be OK for it to be OK? I don't know, something about idempotence. The rationale isn't even fully formed in my head. and now I think it's a bad idea because then someone might reasonably ask what if you send a callback ID that has never been added to that cell? Should it behave differently than a callback ID that was added in the past but was removed? And if it should be different, why? So, error if the callback doesn't exist. |
Ok. I think we're good. |
Thanks, I think so too. I'll use the squash + merge button for this one. |
let new_id = self.cells.len(); | ||
for &id in dependencies { | ||
match self.cells.get_mut(id) { | ||
Some(c) => c.dependents.push(new_id), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unsafe. If this function gets called with a valid dependency followed by an invalid one, an entry is added to the valid cell's dependents, and then the dependent cell is not created. We have to check all cells' validity first.
I felt it would be educational for me to try to implement this in Rust.
Edit: After completing it: Very much so. Lifetimes, closures, storing closure in structs (you have to be generic over the closure type, and give it a lifetime, also understand FnMut vs Fn)
Tests will be taken from the Go track. In case you wonder why there isn't a JSON file... the JSON file for this problem probably at best can only have rough descriptions of how the tests should go.
In some instances, I am unsure of the API that should be presented to students. When I am unsure, I will solicit feedback. After that, I'll make a stub file.Edit after having attempted the problem many ways: I'm pretty sure this interface is simple and works. Whereas an interface that gives direct access to the cells is fraught with problems. You can see some failed attempts in the commit history.
Reviewers, please pay attention to the following questions. I am satisfied with my current answer to all of them, but would gratefully accept ideas on them.
remove_callback
on an already-removed callback should beOk
orErr
? I simply didn't test the exact result, only that it doesn't interfere with other callbacks.Fn
andFnMut
, so we can't avoid them completely. But maybe we want to decrease the amount of work required as much as we can.