Why does `Ref(Mut)::map` require HRTB `FnOnce`?

The signature of Ref::map looks like this

pub fn map<U: ?Sized, F>(orig: Ref<'b, T>, f: F) -> Ref<'b, U>
where
    F: FnOnce(&T) -> &U // πŸ‘ˆ HRTB!!
{}

I was wondering why it couldn't be like this instead

pub fn map<U: ?Sized, F>(orig: Ref<'b, T>, f: F) -> Ref<'b, U>
where
    F: FnOnce(&'b T) -> &'b U // πŸ‘ˆ fixed 'b lifetime
{}

&'b T would be a reference to the contents of the RefCell that can outlive the Ref and therefore outlive the runtime borrow check. That would be unsound.

1 Like

I'm not the OP but I don't follow. Would you mind expanding on that?

The place the lifetime 'b in Ref<'b, T> comes from originally is RefCell::borrow() β€” it is the lifetime of a reference to the RefCell. It ensures that the Ref cannot outlive the RefCell. However, it can be arbitrarily long β€” even 'static β€” if the caller provides a suitable &'b RefCell<T>.

But the premise of a RefCell is that you can borrow the contents only as long as you have the run-time Ref or RefMut guard alive. That's how it prevents conflicts. And so any reference to the T you get must be a borrow of the Ref, not of the RefCell.

If Ref::map() had the signature proposed by @wishawa, then you could use it to break RefCell by extracting a long-lived &T:

use std::cell::{RefCell, Ref};

fn cheat<'rc, T>(cell: &'rc RefCell<T>) -> &'rc T {
    let guard = cell.borrow();
    let mut output = None;
    Ref::map(guard, |ref_to_contents| {
        output = Some(ref_to_contents);
        &()
    });
    output.unwrap()
}

fn main() {
    let cell = RefCell::new(0);
    let r1: &i32 = cheat(&cell);
    let r2: &mut i32 = &mut *cell.borrow_mut();
    // oops, UB: r1 and r2 refer to the same memory but r2 is exclusive
}

There is, however a valid non-HRTB signature you could write:

pub fn map<'r, U: ?Sized, F>(orig: &'r Ref<'b, T>, f: F) -> Ref<'r, U>
where
    F: FnOnce(&'r T) -> &'r U
{...}

Here, the f function is given a reference that is only valid as long as the Ref exists. But, in order to achieve that, it has to take a borrowed Ref, and that is less useful than the real Ref::map() β€” if you can keep the original Ref around to borrow it, then you can also just apply the function to the reference the guard dereferences to:

let guard = cell.borrow();
let u: &U = f(&*guard);
6 Likes

Okay, I didn't realize where the 'b came from. Thanks!

You can actually reason this out without looking at any of the context at all, just from knowing how the type system works:

Any time you see a lifetime parameter in a type, like the 'b in Ref<'b, T>, you know that the lifetime outlives that type β€” whatever it refers to must be something that will continue to exist past all instances of that type.

Therefore, if you know that Ref<'b, T> is necessary and sufficient to take references to the content of the cell, you can conclude that 'b cannot possibly be a suitable lifetime for references to the content, because 'b must outlive Ref<'b, T>, so it must be too long to be sound.

(Except in the trivial case where something else forces 'b to be exactly as long as the Ref exists, but that's uninteresting because you cannot do much with it β€” similar to the mistake beginners sometimes make of setting all lifetimes equal and producing the perma-borrowed &'a mut Foo<'a>, which you often can't even drop.)

2 Likes

This function is relatively useless, as the point of Ref<'a, T> is that what you really wanted is a &'a T reference, but soundness requires a destructor to be executed when the reference is dropped. In this case, from a &'r Ref<'b, T> and a FnOnce(&'r T) -> &'r U, you can of course construct a real &'r U trivially.

Turns out, with a bit of trickery, this function implementable nonetheless, using the original map; and the approach is to first create the real &'r U and subsequently sneak it back into the mapped Ref using a PhantomData<&'b ()> to create implied bounds to limit the lifetimes on the HRTB of the outer Ref::map call:

use std::{cell::Ref, marker::PhantomData};

fn alternative_map<'b, 'r, T: ?Sized, U: ?Sized, F>(orig: &'r Ref<'b, T>, f: F) -> Ref<'r, U>
where
    F: FnOnce(&'r T) -> &'r U,
{
    let r: &'r U = f(orig);
    Ref::map(Ref::map(Ref::clone(orig), |_| &PhantomData::<&'b ()>), |_| r)
}
1 Like

I understand now. Thank all of you! I should've asked on URLO or SO first :sweat_smile:.

My original problem was that sometimes the closure can only handle lifetimes not longer than 'b. What I want now would be something like

pub fn map<U: ?Sized, F>(orig: Ref<'b, T>, f: F) -> Ref<'b, U>
where
    F: for<'x where 'b: 'x> FnOnce(&'x T) -> &'x U
{}

I suppose that for<'x where 'b: 'x> is not going to be a thing anytime soon. It can be achieved with a new trait too, but adding that to the standard library just for this seems unjustified.

You can create such bounds using implicit bounds from additional closure arguments. E.g.

fn map_bounded<'b, 'r, T: ?Sized, U: ?Sized, F>(orig: Ref<'b, T>, f: F) -> Ref<'b, U>
where
    F: for<'x> FnOnce(&'x T, PhantomData<&'x &'b ()>) -> &'x U,
{
    todo!()
}

where &'x &'b () creates an implicit 'b: 'x bound.


A way to implement this function without using unsafe is via a transparent wrapper type that mentions the lifetime. E.g. as follows:

use std::{cell::Ref, marker::PhantomData};

fn map_bounded<'b, 'r, T: ?Sized, U: ?Sized, F>(orig: Ref<'b, T>, f: F) -> Ref<'b, U>
where
    F: for<'x> FnOnce(&'x T, PhantomData<&'x &'b ()>) -> &'x U,
{
    use ref_cast::RefCast;
    #[derive(RefCast)]
    #[repr(transparent)]
    struct WithPhantom<T: ?Sized, P>(PhantomData<P>, T);
    Ref::map(
        Ref::map(orig, |x| WithPhantom::<_, &'b ()>::ref_cast(x)),
        |x| f(&x.1, PhantomData),
    )
}

This is amazing! Thank you so much. Can I use the example code in a project under MPL 2 license?

Sure, that’s barely any code at all, use it however you like :sweat_smile:

1 Like

More precisely the contents would outlive the runtime borrow check if the Ref ends up being dropped in the first place. Arguably a minute detail here since having map that requires callers to leak their borrow state would be so ergonomically flawed it wouldn't have been acceptable either. But, important, Ref{,Mut}::leak is sound (, not-yet-stable), and still has the signature that would allow this.

 fn (_: Ref<'b, T>) -> &'b T

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