Announcing borrow-bag, a heterogeneous collection with zero-cost add and borrow

A type-safe, heterogeneous collection with zero-cost add and borrow.
https://crates.io/crates/borrow-bag

Initially created to solve a problem in another project I'm collaborating on, but generic enough to be released on its own. It's my first adventure in type-level programming, and Rust made it very pleasant. It solves the problem of having a heterogeneous collection of owned values, with an easy / type-safe way to borrow them later but still retain the ability to move them in the interim. Structurally similar to frunk's HList, but with a tiny API:

use borrow_bag::new_borrow_bag;

struct X(u8);
struct Y(u8);

fn main() {
    let bag = new_borrow_bag();
    let (bag, x_handle) = bag.add(X(1));
    let (bag, y_handle) = bag.add(Y(2));

    let x: &X = bag.borrow(x_handle);
    assert_eq!(x.0, 1);
    let y: &Y = bag.borrow(y_handle);
    assert_eq!(y.0, 2);
}

Feedback / review welcome.

2 Likes

Is the let x: &X required or can it be just let x?

Type inference works fine with this API, so the following example works exactly as you'd expect:

use borrow_bag::new_borrow_bag;

struct X(u8);
struct Y(u8);

fn main() {
    let bag = new_borrow_bag();
    let (bag, x_handle) = bag.add(X(1));
    let (bag, y_handle) = bag.add(Y(2));

    let x = bag.borrow(x_handle);
    assert_eq!(x.0, 1);
    let y = bag.borrow(y_handle);
    assert_eq!(y.0, 2);
}

I only annotated the types in the original example for clarity.

Could you please explain me what's a possible use case for this?

That's a really good question. It solves my particular problem, so I can only really speak about what my problem was.

I won't speak in specifics, since we're not ready to announce our project yet, but I've come up with the following contrived example which has a request/result scenario with support for an "around" hook:

Apologies for the complexity of this example. In spite of my efforts to simplify it, this structure of traits is necessary to demonstrate the problem I ran into, particularly in that the AroundHook can't be made into a trait object because call takes a closure.

In this example where we have only one Dispatcher, the ownership is straightforward because we can store the hook where it's going to be used. There are a few complicating factors as this grows in scope:

  1. The Registry will have multiple Dispatcher values, and will need to determine which to use based on some field(s) of the Input
  2. We want to define the hooks only once, and potentially use them in many places, so our options are either to clone them or borrow them
  3. If we opt to clone hooks, duplicating AroundHook values for every dispatcher requires a lot of clone() calls as the Registry grows, but perhaps worse imposes a need for the AroundHook to be Clone. Not every type can be Clone, so that's too limiting
  4. If we opt to store a reference instead, we run into the limitation that AroundHook can't be made into a trait object, because of the generic function which takes a closure, so we can only store a reference to the concrete type.

I couldn't get the ownership to work correctly when using references. One example:

BorrowBag solves this by allowing a BorrowBag<V> owning all the hooks to be stored in Registry, and passed through the dispatch call. The DispatcherImpl struct can store as many Handle<T, _> values as it needs to describe all the hooks it will use, and then during dispatch can use those handles to borrow the hook values and dispatch.