-
Notifications
You must be signed in to change notification settings - Fork 13.3k
Add std::cell::Ref::map and friends #25747
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
Conversation
Thanks for the pull request, and welcome! The Rust team is excited to review your changes, and you should hear from @alexcrichton (or someone else) soon. If any changes to this PR are deemed necessary, please add them as extra commits. This ensures that the reviewer can see what has changed since they last reviewed the code. The way Github handles out-of-date commits, this should also make it reasonably obvious what issues have or haven't been addressed. Large or tricky changes may require several passes of review and changes. Please see CONTRIBUTING.md for more information. |
https://github.com/rust-lang/rfcs#when-you-need-to-follow-this-process lists "Additions to Unresolved questions:
|
62e8200
to
2b49fc6
Compare
I've added and experimented with this once. I scrapped the idea for my use case, because this makes it easy to return |
Wait, why don't you use a closure here? |
What borrowing error do you mean? This design of taking |
By borrowing error I mean a panic in |
Updated |
Sigh. I realized the PR as-is introduces memory unsafety: the closure can keep around the pointer it’s given longer than the lifetime of the #![feature(core)]
use std::cell::{RefCell, Ref, map_ref};
struct Foo {
destroyed: bool
}
impl Drop for Foo {
fn drop(&mut self) {
self.destroyed = true;
}
}
fn main() {
let a = RefCell::new(Box::new(Foo { destroyed: false }));
let mut b = None;
let _: Option<Ref<()>> = map_ref(a.borrow(), |s| {
b = Some(&**s);
None
});
println!("{}", b.unwrap().destroyed);
*a.borrow_mut() = Box::new(Foo { destroyed: false });
println!("{}", b.unwrap().destroyed); // Use after free
} I think making |
It looks like the closure idea was broken then. Sorry about that, my bad. |
cc #19220
I suspect not; couldn't one make |
Right. Here is an alternative idea. |
There's prior art that should definitely be considered at #19220. |
I believe this version is sound, it looks good. Why use an optional return value though? |
My use case it returning a new Example in Kuchiki: pub struct ElementData {
pub name: QualName,
pub attributes: RefCell<HashMap<QualName, String>>,
}
impl ElementData {
fn get_attribute(&self, name: Atom) -> Option<Ref<str>> {
// With `map_ref` as proposed:
map_ref(self.attributes.borrow(), |attrs| {
attrs.get(&QualifiedName { ns: ns!(""), local: name }).map(|s| &**s)
})
// With a simplified `map_ref`, two HashMap lookups:
let borrow = self.attributes.borrow();
let key = QualifiedName { ns: ns!(""), local: name };
if borrow.contains_key(&key) {
Some(map_ref(borrow, |attrs| &*attrs[&key]))
} else {
None
}
}
} |
Unlike #19220, this doesn’t change the semantic of existing items (it just adds a new one), so I don’t think it has the same issues. The example program at #19220 (comment) correctly fails to build with a lifetime error (after updating it for language changes). |
Maybe my |
I, too, have wanted this kind of functionality from time to time before, and I'm totally fine seeing it prototyped! This is a minor enough API that I don't believe it will require an RFC, but I'll cc @rust-lang/libs to see if other have opinons as well :) I have the same question as @bluss as well in that from an API perspective, could you elaborate on why the closure returns
Naming-wise this may also wish to consider
We've been quite wary of adding methods on types that implement Apart from the |
Regarding pub enum NodeData {
Element(ElementData),
Text(String),
Comment(String),
Doctype(Doctype),
Document(DocumentData),
}
pub struct Node {
// ...
pub data: RefCell<NodeData>,
}
impl Node {
fn as_element(&self) -> Option<Ref<ElementData>> {
map_ref(self.data.borrow(), |data| match *data {
Element(ref element) => Some(element),
_ => None
})
}
} If pub fn map_ref<'b, T: ?Sized, U: ?Sized, F>(orig: Ref<'b, T>, f: F) -> Ref<'b, U>
where F: FnOnce(&T) -> &U … this would be more tricky to implement and require matching twice: impl Node {
fn as_element(&self) -> Option<Ref<ElementData>> {
let borrow = self.data.borrow();
match *borrow {
Element(_) => map_ref(borrow, |data| match *data {
Element(ref element) => element,
_ => unreachable!()
}),
_ => None
}
}
} I mentioned before that maybe I could instead return |
Added How can I run the doctests? |
I’ve found out (thanks eddyb!) that I can run the doctests with |
I'm worried about the compositionality of this API, however. For example if I have a I would personally rather see this as Also, it looks like associated functions can indeed be defined that aren't methods: struct A;
impl A {
fn foo(a: &A) {}
}
let a = A;
a.foo(); // error
A::foo(&a); // ok Perhaps static |
Using static functions sounds great unless there is any plan to expand UFCS and make them resolve with method syntax. |
I agree that using a #![feature(core)]
use std::cell::*;
// So that I don’t have to re-bootrap...
fn simplified_map_ref<'b, T, U, F>(orig: Ref<'b, T>, f: F) -> Ref<'b, U>
where F: FnOnce(&T) -> &U {
map_ref(orig, move |v| Some(f(v))).unwrap()
}
fn main() {
let x: RefCell<Result<u32, ()>> = RefCell::new(Ok(5));
simplified_map_ref(x.borrow(), |r| &r.ok());
} a.rs:11:41: 11:47 error: borrowed value does not live long enough
a.rs:11 simplified_map_ref(x.borrow(), |r| &r.ok());
^~~~~~
note: in expansion of closure expansion
a.rs:11:36: 11:47 note: expansion site
a.rs:11:40: 11:47 note: reference must be valid for the anonymous lifetime #1 defined on the block at 11:39...
a.rs:11 simplified_map_ref(x.borrow(), |r| &r.ok());
^~~~~~~
a.rs:11:40: 11:47 note: ...but borrowed value is only valid for the block at 11:39
a.rs:11 simplified_map_ref(x.borrow(), |r| &r.ok());
^~~~~~~
error: aborting due to previous error |
I like the idea of using static methods on |
If we decide to do this I don’t see an obstacle to this: we can tell such functions apart from methods by them not using |
As discussed on IRC with @alexcrichton, I modified the PR to have both |
#[unstable(feature = "core", reason = "recently added")] | ||
#[inline] | ||
pub fn map<U: ?Sized, F>(orig: RefMut<'b, T>, f: F) -> RefMut<'b, U> | ||
where F: FnOnce(&mut T) -> &mut U { |
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.
Stylistically we tend to format functions like this as:
pub fn foo()
where ...
{
// ...
}
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.
Done.
Here is an attempt at generalizing impl<'b: 'c, 'c, T: ?Sized> Ref<'b, T> {
pub fn generalized_map<U: ?Sized, F, R>(orig: Ref<'b, T>, f: F) -> R
where F: FnOnce(&T, &FnOnce(&'c U) -> Ref<'c, U>) -> R {
f(orig._value, &move |new| Ref {
_value: new,
_borrow: orig._borrow,
})
}
} But using it doesn’t borrow-check: let x: RefCell<Result<u32, ()>> = RefCell::new(Ok(5));
let b: Option<Ref<u32>> = Ref::generalized_map(x.borrow(), |r, new_ref| match *r {
Ok(ref value) => Some(new_ref(value)),
Err(_) => None,
});
assert_eq!(*b.unwrap(), 5);
} I’m not sure if I’m doing something wrong or if this is not possible to express in current Rust. Regardless, a function that takes a closure that takes another closure is quite convoluted, not a great API. |
/// This is an associated function that needs to be used as `Ref::clone(...)`. | ||
/// A `Clone` implementation or a method would interfere with the widespread | ||
/// use of `r.borrow().clone()` to clone the contents of a `RefCell`. | ||
#[unstable(feature = "core", |
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.
Could these features switch to something other than "core"? Something like cell_extras
would be fine.
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.
Done.
This API looks good to me, and I also believe the implementation is sound, so I'm all good with this! I'd like to hear more opinions about |
+1 to the basic idea here.
This one is tricky. In the limit, with full inherent associated items, many "single type" modules could be flattened to just the type, and this is one step further in that direction. The main advantage is that you're usually importing the type already, so this saves you an additional import of the module/function. To be honest, I think it's inevitable that APIs will drift in this direction, and don't see much reason to fight it in (In the long run, this means that true free functions are predominantly going to be code that is not clearly associated with any particular type.)
I think the level of fanciness in the current PR is appropriate. |
Ok, thanks again for the PR @SimonSapin! r=me with a squash |
…::Ref ... and generalize the bounds on the value type.
See design discussion in rust-lang#25747
Squashed into two commits. |
⌛ Testing commit 64e72b0 with merge b597347... |
💔 Test failed - auto-linux-64-nopt-t |
See design discussion in rust-lang#25747
Oh noes. Should be fixed, with this diff before squashing: diff --git a/src/libcoretest/lib.rs b/src/libcoretest/lib.rs
index 78c7215..3d14b3f 100644
--- a/src/libcoretest/lib.rs
+++ b/src/libcoretest/lib.rs
@@ -24,6 +24,7 @@
#![feature(step_by)]
#![feature(slice_patterns)]
#![feature(float_from_str_radix)]
+#![feature(cell_extras)]
extern crate core;
extern crate test; |
For slightly complex data structures like `rustc_serialize::json::Json`, it is often convenient to have helper methods like `Json::as_string(&self) -> Option<&str>` that return a borrow of some component of `&self`. However, when `RefCell`s are involved, keeping a `Ref` around is required to hold a borrow to the insides of a `RefCell`. But `Ref` so far only references the entirety of the contents of a `RefCell`, not a component. But there is no reason it couldn’t: `Ref` internally contains just a data reference and a borrow count reference. The two can be dissociated. This adds a `map_ref` function that creates a new `Ref` for some other data, but borrowing the same `RefCell` as an existing `Ref`. Example: ```rust struct RefCellJson(RefCell<Json>); impl RefCellJson { fn as_string(&self) -> Option<Ref<str>> { map_ref(self.borrow(), |j| j.as_string()) } } ``` r? @alexcrichton
See design discussion in rust-lang#25747
For slightly complex data structures like
rustc_serialize::json::Json
, it is often convenient to have helper methods likeJson::as_string(&self) -> Option<&str>
that return a borrow of some component of&self
.However, when
RefCell
s are involved, keeping aRef
around is required to hold a borrow to the insides of aRefCell
. ButRef
so far only references the entirety of the contents of aRefCell
, not a component. But there is no reason it couldn’t:Ref
internally contains just a data reference and a borrow count reference. The two can be dissociated.This adds a
map_ref
function that creates a newRef
for some other data, but borrowing the sameRefCell
as an existingRef
.Example:
r? @alexcrichton