Borrow question

I want to create a nice API to be able to simulate dice rolls, but I'm facing a lifetime issue.

I want to be able to use a syntax closer to my business vocabulary, and be able to manipulate dices with a given number of faces.

In pseudo-Rust, what I want to do is:

let rng = rand::thread_rng();
let d10 = create_wrapper_that_returns numbers_between_1_to_10(&mut rng);
let d6 = create_wrapper_that_returns numbers_between_1_to_6(&mut rng);

let usage = d10.roll() + d6.roll();

But obviously I can't capture rng by mutable reference two times at the same time. If I was using rng.gen_range(0, number_of_faces) directly, I wouldn't be facing borrowing issue. I don't understand how I can rewrite my code.

The only constraint I have is that roll() should neither take rng or number_of_faces as argument (with anything other than self), otherwise the wrapper is useless.

A more detailed example. Note: The real code isn't simple closure, but a struct Dice { rng: &mut rand_core::RngCore, number_of_faces: usize }, and has more associated methods but the idea is the same.

You can call rand::thread_rng() twice and pass each one to one of the wrappers.

It works for rand::thread_rng(), but not for all types implementing RngCore. Most notably the default mock (StepRng) doesn't implement copy. This means that I would restrict the use of my function using dices just because I want to have a nicer syntax around gen_range().

You can make something cloneable and possible to mutate from multiple places if you wrap it in Arc<Mutex<…>>.

If you create MyRng(Arc<Mutex<StepRng>>) sandwitch, you might be able to implement Rng interface for it.

Of course, but this would have an impact both in memory size, and a runtime cost. This code is, and should stay single threaded for reproducibility (it may however be called multiple time in parallel). I feel that the borrow checker ask me to pay a price for something I explicitly don't want (safe parallelism).

IIUC opting out of thread safety by enabling zero-runtime-overhead shared mutability is a textbook example of what Cell/RefCell are good for.

I think everyone else just gave you the more general answer because your first post didn't say anything about wanting to optimize away the synchronization cost by relying on your program staying single-threaded.

@lxrec I am pretty sure you are right. I just need to understand how to use Cell. This definitively feels like the right direction.

I think everyone else just gave you the more general answer because your first post didn't say anything about wanting to optimize away the synchronization cost by relying on your program staying single-threaded.

That's right, it totally my fault.

1 Like

Note: it is possible that I don't understand how to use Cell.

This still didn't solve my issue. Now that I have taken more time to understand the problem, I fear that safe Rust doesn't have the semantic to express what I want. Basically, I want to borrow the rng, when calling the function that roll dices, and not when creating the dice. This way, I can have multiples dices that use the same rng, but cannot be used at the same time.

let rng: impl rand::RngCore = /* some rng */;
let d10 = create_dice(& not_borrowed mut rng, 10); // do **not** borrow rng here
let d6 = create_dice(& not_borrowed mut rng, 6); // nor there

let first_use = d10
    .borrow() // the borrow occurs here
    .roll();
// and ends here

// so we could use it again
let first_use = d6
    .borrow() // the borrow occurs again
    .roll();
// and ends here

Obviously, the borrow could be done in the roll() function, but I wanted to have some chaining to show how exactly it should behave.

Parallelism, mutability, reference: pick 2 or you get a buggy program.

  • In C++, it's easy to have mutability and either (parallelism or reference). It would have made this code easy to write, at the price of writing any code with concurrency.
  • in safe Rust, it's parallelism and either (mutability or reference).

If the borrow checker could be extended to support differed mutability (like it has been extended to support non-lexical lifetime), my sample to be expressed.

You can get mutability + sharing without parallelism with &RefCell<YourRngCoreType>. You can't use Cell because that requires the inner type to be Copy in most use cases.

But RefCell has a runtime cost, doesn't it? This would make the abstraction nen zero cost.

"zero-cost" is a relative term. RefCell wouldn't be in the language if it was always bad to use it. Adding a RefCell to code that doesn't need one certainly adds some unnecessary runtime costs. But when all of the constraints we're talking about come together at the same time, such that a RefCell is actually an appropriate choice, it's likely that any unsafe solution would have to pay the exact same runtime costs to be sound (and end up just being a crude version of a RefCell). Remember that RefCell is cheaper than Arc<Mutex>, precisely because RefCell does not provide thread-safety. And if you happened to have a PRNG that implements Copy, then you could use a regular Cell. Plus, in simple cases like this, the runtime cost of RefCell may get optimized out anyway.

RefCell is just a non-atomic integer increment.

I took the time to think again about my issue, and I finally end-up with was a function roll() that took the rng as parameters. It's not as clean as I would have like but it's better than nothing.

"zero-cost" is a relative term.

Assuming we use the C++ meaning, witch is "zero [runtime] cost [abstraction]", it's not relative. An abstraction is zero-cost if its runtime cost is guaranteed to not any higher than the best possible handcrafted version. Given that if I remove the abstraction, I can remove the integer increment, this means that the abstraction that include a RefCell for this specific scenario isn't zero-cost. I definitively think that RefCell has valid use (just like v-tables have valid use that don't incur extra-cost, because you need the indirection even in the handcrafted version).

Plus, in simple cases like this, the runtime cost of RefCell may get optimized out anyway.

Most probably yes. Anyway, I just just trying to find the [current] limits of the language Rust than trying to solve a real issue.

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