Accepting Cow + Rc/Arc?

What's the canonical way to accept as an argument of a function and store as a field either reference, struct or Rc/Arc of that struct?

Can you give an example of what you're trying to achieve?

You could use the AsRef trait. This just says you can get a reference to &Bar from a type B: AsRef<Bar>.

struct Bar;

struct Foo<B: AsRef<Bar>> {
  bar: B
}

fn make_foo<B: AsRef<Bar>>(bar: B) -> Foo<B> {
  Foo { bar }
}
2 Likes

You can also do

struct Bar;

struct Foo<B: Deref<Target=Bar>> {
  bar: B
}

fn make_foo<B: Deref<Target=Bar>>(bar: B) -> Foo<B> {
  Foo { bar }
}

There is not much of a difference to AsRef<T>.

It depends why do you need this flexibility.

If you just want the caller to be able to pass anything, then this seems reasonable:

fn take_foo(&mut self, foo: &Foo) {
    self.foo = Arc::new(foo);
}

// or potentially slightly more efficient
fn take_foo(&mut self, foo: impl Into<Arc<Foo>>) {
    self.foo = foo.into();
}

In the example I'm always storing Arc, because that's most flexible for the users of the object.

If you want to store the object using various wrappers, to allow callers to have a bit more efficiency at cost of borrow checker or thread-safety limits, that's trickier, because that touches higher-kinded types.

There's Archery crate that does some clever hacks to allow storing either Rc or Arc:

For Cow that's doable if you add a lifetime parameter to your container type, and make callers use 'static type for the Owned case.

struct CowWrapper<'a> {
    cow: Cow<'a, Foo>,
}

impl CowWrapper<'static> {
   fn new_owned(foo: Foo) -> Self {}
}

impl<'a> CowWrapper<'a> {
   fn new_borrowed(foo: &'a Foo) -> Self {}
}
1 Like

So, my use case is:

I have a struct that performs operations on a list of another struct. Sth like:

struct Resource {};

struct Bundle {
  resources: Vec<Resource>
};

impl Bundle {
  pub fn add_resource(&mut self, res: Resource) {
    self.resources.push(res);
  }
}

This works with zero-overhead since Bundle owns its Resource. But I have cases where I need the same Resource to go to multiple Bundle instances as read-only.

So, I have two ways to solve it. I can either have some separate struct that owns all Resource instances and hands references to them to Bundle - which is what I'm doing now.
Or I can accept Rc<Resource> and store it.

The former is pretty inflexible and causes a lot of troubles for my consumers since they have to fight the lifetimes and adds this requirement of a separate place to allocate the resources.

The latter is very flexible and convenient, so I'd prefer to use it.

But I'm wondering if it would be possible to handle all three and somehow allow:

let bundle = Bundle::new();
bundle.add_resource(Resource::new());
let res = Resource::new();
bundle.add_resource(&res);

let res_arc = Arc::new(Resource::new());
bundle.add_resource(res_arc.clone());

This would give a lot of flexibility to my consumers who could then organize their management of Resource to their needs, allowing for the simplest mode:

let bundle = Bundle::new();
bundle.add_resource(Resource::new());

and two "more complex" modes where the resources are stored separately either as Arcs, or directly and only reference is passed to bundle.

The reason why I think it should be possible is that I only really read the Resource struct, and all three - ownership, reference and Arc, allow me to read, right?

So I imagine there to be some version of Cow that can handle owned, borrowed and Arc and return a reference. Or something else (trait?) that would handle such ergonomics.

Am I missing some limitation?

It's not clear to me whether you expect most Bundles to contain all three of &Resource, Resource and Arc<Resource>, which can only be done with an enum, or if it would work to have three+ different "flavors" of Bundle which the consumer can choose between. Because if most consumers don't need the flexibility of allowing any type, wrapping the Resource in an enum adds unnecessary overhead to the simple case.

Here's what I'm thinking of:

struct Bundle<R> {
    resources: Vec<R>,
}

use std::borrow::Borrow;

impl<R> Bundle<R> {
    pub fn add_resource(&mut self, res: impl Into<R>) {
        self.resources.push(res.into());
    }

    pub fn use_resources(&self)
    where
        R: Borrow<Resource>,
    {
        for foo in &self.resources {
            // use foo.borrow() to access the Resource
        }
    }
}

This has several advantages:

  • When the consumer doesn't need flexibility, you don't pay for it: Bundle<Resource> can be used in the obvious way with only owned values.
  • Most of the remaining flexibility can be achieved with Bundle<Arc<Resource>> (or Rc in single threaded contexts): you can pass a Resource to add_resource and it will be automatically packaged into an Arc.
  • Bundle<Cow<'a, Resource>> also works. You can make passing an owned or borrowed Resource to add_resource work by implementing Into.
  • If you really want to allow a single Bundle to contain all three, you can still write an enum, implement Borrow for it and use Bundle<ResourceEnum<'a>>. Be aware that implementing Borrow comes with stipulations: Eq, Ord and Hash have to behave the same as for the borrowed type. See the Borrow documentation for more detail.

The main disadvantage - if it is a disadvantage - is that the creator of the Bundle has to know how it will be used. In other words, you can't use Bundle<Arc<Resource>> when you don't know at compile time whether a given Bundle will or will not be used to store references. And of course if all of your use cases might store references, Arcs and owned Resources together, making Bundle generic is more complex than just baking in the enum.

The reason I used Borrow, instead of AsRef as @Michael-F-Bryan did, is because Borrow<T> is implemented for all T. You could use AsRef by implementing AsRef<Resource> for Resource, but I think Borrow<Resource> just seems more semantically appropriate for abstracting over different ways of storing Resources.

1 Like
bundle.add_resource(&res);

If you store &res as a reference (without copying it into Arc), then Rust will have to ensure that lifetime of bundle is shorter than lifetime of res, with all the borrow checker complications it brings.

And borrow checking is done compile time based on types, not at runtime based on usage, so even if bundle user never actually inserts any references, they will still have to deal with lifetimes.

So if ease of use is your goal, taking argument as impl Into<Arc<Resource>> seems the best.

Storing Vec<Cow<'a, Resource>> in Bundle<'a> is the second best option, where you can have a special case of Bundle<'static> that's good only for Cow::Owned variants.

@trentj - this is great!!! I was able to turn FluentBundle to operate on generics and I now have a PR that handles all my needs! Thank you so much!

Here's the PR in case anyone is interested - Turn FluentBundle to be a generic over Borrow<FluentResource> by zbraniecki · Pull Request #114 · projectfluent/fluent-rs · GitHub

1 Like

Great! I'm glad I was able to help.

1 Like

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.