-
Notifications
You must be signed in to change notification settings - Fork 20
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
Cyclic Arc and Rc Builder #90
Comments
example of why struct Node<N, E> {
edges: Box<[Arc<Edge<N, E>>]>,
data: N,
}
struct Edge<N, E> {
nodes: [Weak<Node<N, E>>; 2],
data: E,
}
struct Graph<N, E> {
nodes: Box<[Arc<Node<N, E>>]>,
}
impl<N, E> Graph<N, E> {
fn new(
nodes: impl IntoIterator<Item = N>,
edges: impl IntoIterator<Item = (usize, usize, E)>,
) -> Self {
struct NodeBuilder<N, E> {
node: CyclicArcBuilder<Node<N, E>>,
data: N,
edges: Vec<Arc<Edge<N, E>>>,
}
let builders: Vec<_> = nodes
.map(|data| NodeBuilder {
node: CyclicArcBuilder::new(),
data,
edges: Vec::new(),
})
.collect();
for (n1, n2, data) in edges {
let edge = Arc::new(Edge {
nodes: [builders[n1].node.weak(), builders[n2].node.weak()],
data,
});
builders[n1].edges.push(edge.clone());
builders[n2].edges.push(edge.clone());
}
Graph {
nodes: builders
.map(|b| {
b.node.build(Node {
edges: b.edges.into(),
data: b.data,
})
})
.collect(),
}
}
} |
I really want this feature in my project so I'm super interested into this! BTW, I found there is an another alternative safe solution which is possible under limited conditions.
let mut maybe_error = None;
let rc = Rc::new_cyclic(|weak| {
match T::try_obj(weak) {
Ok(obj) => obj,
Err(e) => {
maybe_error = Some(e);
T::default()
}
}
});
match maybe_error {
Some(e): Err(e),
None: Ok(rc),
} |
Would really like to see the support for async. Since this specific pattern is not bounded to sync or async context. The current lack of cyclic builder for async context actually creates a feature disparity. Recently I tried to refactor some of my code from using threadpool into async executors. And there is simply no replacement for |
@ZhennanWu Just like with fallible variant, why can't you await the async stuff before the initialization closure and keep it only for assigning the fields? |
There are authentic cyclic reference scenarios that a late assignment is too difficult to implement. Like registering a callback on another notification source. A late assignment would require tracking all touched sources. On top of that, since it is about parallel async runtime, compromising thread safety is a no go. A "late assignment" is just semantically different to the "interior mutability" semantics provided by |
Thanks for your ACP. We discussed this in the libs meetup. Instead of calling this a 'builder' with a 'build' method, we'd much rather see something like a Feel free to open a new ACP for that if you want feedback on the API, or open a PR to rust-lang/rust directly if you feel like implementing it. |
i'd expect to also have a |
Thank you for the feedback. I will put together a PR to implement this and we can do any needed API iteration on the PR. I'll make sure to include both |
I just posted a PR implementing |
This is an `Rc` that is guaranteed to only have one strong reference. Because it is uniquely owned, it can safely implement `DerefMut`, which allows programs to have an initialization phase where structures inside the `Rc` can be mutated. The `UniqueRc` can then be converted to a regular `Rc`, allowing sharing and but read-only access. During the "initialization phase," weak references can be created, but attempting to upgrade these will fail until the `UniqueRc` has been converted to a regular `Rc`. This feature can be useful to create cyclic data structures. This API is an implementation based on the feedback provided to the ACP at rust-lang/libs-team#90.
Add `alloc::rc::UniqueRc` This PR implements `UniqueRc` as described in rust-lang/libs-team#90. I've tried to stick to the API proposed there, incorporating the feedback from the ACP review. For now I've just implemented `UniqueRc`, but we'll want `UniqueArc` as well. I wanted to get feedback on this implementation first since the `UniqueArc` version should be mostly a copy/paste/rename job.
Comparing rust-lang/rust#112566 with the original proposal, there seems to be one downside: with |
This is an `Rc` that is guaranteed to only have one strong reference. Because it is uniquely owned, it can safely implement `DerefMut`, which allows programs to have an initialization phase where structures inside the `Rc` can be mutated. The `UniqueRc` can then be converted to a regular `Rc`, allowing sharing and but read-only access. During the "initialization phase," weak references can be created, but attempting to upgrade these will fail until the `UniqueRc` has been converted to a regular `Rc`. This feature can be useful to create cyclic data structures. This API is an implementation based on the feedback provided to the ACP at rust-lang/libs-team#90.
Add `alloc::rc::UniqueRc` This PR implements `UniqueRc` as described in rust-lang/libs-team#90. I've tried to stick to the API proposed there, incorporating the feedback from the ACP review. For now I've just implemented `UniqueRc`, but we'll want `UniqueArc` as well. I wanted to get feedback on this implementation first since the `UniqueArc` version should be mostly a copy/paste/rename job.
With |
There could be a different way to simplify creating of cyclic Arcs - first you create a Weak reference with zero strong count, which is clone-able, but upgrading it will give Then this weak reference is upgraded to full-fledged Something like this (API intentionally resembles of let mut weak: Weak<Gadget> = Weak::uninit();
let gadget = Gadget::new(weak.clone()).await?;
weak.write(gadget);
let strong: Arc<Gadget> = weak.assume_init().unwrap();
Ok(strong);
The downside of such approach is that there would be a store-store race condition if multiple threads would try to call This could be avoided by following the let weak: Weak<Gadget> = Weak::new();
let gadget = Gadget::new(weak.clone()).await?;
let strong: Arc<Gadget> = weak.upgrade_or_init(|| gadget);
Ok(strong); But since |
I ended up with an API which looks like this: let uninit = ArcUninit::uninit();
let weak = uninit.downgrade();
assert!(weak.upgrade().is_none());
// if ArcUninit is dropped before initializing (for example, in case of fallible constructor),
// the storage will be deallocated after weak is dropped
// this also, of course, works across await boundaries, since ArcUninit is Send + Sync.
let arc = uninit.init(Gadget{ me: weak.clone() });
let arc_from_weak = weak.upgrade().unwrap();
assert!(Arc::ptr_eq(&arc, &arc_from_weak); This API works well with async and fallible constructors, and also allows things like creating multiple Arcs simultaneously with weak cross-references like: struct CrossLink {
other: Weak<CrossLink>,
}
let link1 = ArcUninit::uninit();
let link2 = ArcUninit::uninit();
let weak1 = link1.downgrade();
let weak2 = link2.downgrade();
let arc1 = link1.init(Gadget{ other: weak2 });
let arc2 = link2.init(Gadget{ other: weak1 });
assert!(Arc::ptr_eq(&arc1, &arc2.other.upgrade().unwrap())); |
Proposal
Problem statement
It can be helpful to build cyclic ARC structures. Currently there is
Arc::new_cyclic
, but this does not work if the supplieddata_fn
returns aResult
or is anasync fn
.Motivation, use-cases
Consider the following example that creates a cyclic
Arc
structure usingArc::new_cyclic
:We may want to make
Gadget::new
be an async function that returns aResult
indicating it might fail. We might be tempted to replace theArc::new_cyclic(|gadget| Gadget::new(gadget.clone()))
withArc::new_cyclic(|gadget| async { Gadget::new(gadget.clone()).await? })
, but doing so gives the following errors:There is currently no obvious way to make this work using Rust, and many if not all of the non-obvious ways involve unsafely depending on
Arc
implementation details.Solution sketches
We propose adding a new
CyclicArcBuilder
and correspondingCyclicRcBuilder
with roughly the following API:As an alternative (or in addition) to
CyclicArcBuilder::new
, we could also add anew_cyclic_builder()
method toArc
.Using this API would allow us to solve the problem in the previous section as follows:
Alternative: More
cyclic
ConstructorsAn alternative we considered was adding a collection of
new_cyclic
variants, liketry_new_cyclic
,async_new_cyclic
, andasync_try_new_cyclic
. This approach adds a lot of API surface whileCyclicArcBuilder
subsumes all of these use cases and more (for example, the originalarc_new_cyclic
issue mentioned multiple arity variants).Alternative: Keyword Generics
The Keyword Generics Initiative would make it possible to use the existing
Arc::new_cyclic
constructor in fallible and async contexts by sharing a single implementation. The initiative is still in the early stages though, while addingCyclicArcBuilder
would allow making incremental progress sooner.CyclicArcBuilder
is also strictly more general, since it can enable the multi-arity use case mentioned above while keyword generics would not.One advantage of both of these alternatives is that they are likely to present a simpler API.
CyclicArcBuilder
is quite general, but arguably not as ergonomic. However, the kind of code that requires something likenew_cyclic
would likely already in a situation that would benefit from an even more flexible API.Alternative: Multi-phase Construction
One option is to split the construction of the object into a fallible and infallible portion, like this example take from this comment:
This approach does work, but there are some downsides. It requires constructing the object in an invalid state and then patching it up afterwards, which is likely more error-prone than being able to make the object correct-by-construction using the builder API.
One issue this this approach is that it can either expose implementation details that should be private, or require factoring the code differently. Suppose we wanted to have a
ServiceManager
that managed multiple objects that implement theService
trait. UsingCyclicArcBuilder
, we might write something like this:Without
CyclicArcBuilder
,ServiceManager::new
would either need access to the service internals, or we'd need to add a method to theService
trait to finalize construction. The first option would look like this:We can work around needing direct access to the
service_manager
field by using something like anactivate
method:This avoids leaking internal details of the service implementation, but it still less than ideal. One, it requires complicating the
Service
trait with anactivate
method. More significantly though, this approach is a maintenance hazard. If in the future we addedbaz_service
toServiceManager
, the compiler would guarantee that we callBazService::create()
but it would be easy to forget to callobj.baz_service.activate(this.clone())
. On the other hand, the builder pattern makes this mistake impossible.Links and related work
This proposal builds upon and generalizes some of the alternatives discussed in Tracking Issue for
arc_new_cyclic
(#75861). Some specific comments have been linked above, but the whole thread is relevant.What happens now?
This issue is part of the libs-api team API change proposal process. Once this issue is filed the libs-api team will review open proposals in its weekly meeting. You should receive feedback within a week or two.
The text was updated successfully, but these errors were encountered: