% Peeking

Alright, we made it through push and pop. I'm not gonna lie, it got a bit emotional there. Compile-time correctness is a hell of a drug.

Let's cool off by doing something simple: let's just implement peek_front. That was always really easy before. Gotta still be easy, right?

Right?

In fact, I think I can just copy-paste it!

pub fn peek_front(&self) -> Option<&T> {
    self.head.as_ref().map(|node| {
        &node.elem
    })
}

Wait. Not this time.

pub fn peek_front(&self) -> Option<&T> {
    self.head.as_ref().map(|node| {
        &node.borrow().elem
    })
}

HAH.

cargo build
   Compiling lists v0.1.0 (file:///Users/ABeingessner/dev/too-many-lists/lists)
src/fourth.rs:64:14: 64:27 error: borrowed value does not live long enough
src/fourth.rs:64             &node.borrow().elem
                              ^~~~~~~~~~~~~
note: in expansion of closure expansion
src/fourth.rs:63:32: 65:10 note: expansion site
src/fourth.rs:62:44: 66:6 note: reference must be valid for the anonymous lifetime #1 defined on the block at 62:43...
src/fourth.rs:62     pub fn peek_front(&self) -> Option<&T> {
src/fourth.rs:63         self.head.as_ref().map(|node| {
src/fourth.rs:64             &node.borrow().elem
src/fourth.rs:65         })
src/fourth.rs:66     }
src/fourth.rs:63:39: 65:10 note: ...but borrowed value is only valid for the block at 63:38
src/fourth.rs:63         self.head.as_ref().map(|node| {
src/fourth.rs:64             &node.borrow().elem
src/fourth.rs:65         })
error: aborting due to previous error
Could not compile `lists`.

Ok I'm just burning my computer.

This is exactly the same logic as our singly linked stack. Why are things different. WHY.

The answer is really the whole moral of this chapter: RefCells make everything sadness. Up until now, RefCells have just been a nuisance. Now they're going to become a nightmare.

So what's going on? To understand that, we need to go back to the definition of borrow:

fn borrow<'a>(&'a self) -> Ref<'a, T>
fn borrow_mut<'a>(&'a self) -> RefMut<'a, T>

In the layout section we said:

Rather than enforcing this statically, RefCell enforces them at runtime. If you break the rules, RefCell will just panic and crash the program. Why does it return these Ref and RefMut things? Well, they basically behave like Rcs but for borrowing. They keep the RefCell borrowed until they go out of scope. We'll get to that later.

It's later.

Ref and RefMut implement Deref and DerefMut respectively. So for most intents and purposes they behave exactly like &T and &mut T. However, because of how those traits work, the reference that's returned is connected to the lifetime of the Ref, and not actual RefCell. This means that the Ref has to be sitting around as long as we keep the reference around.

This is in fact necessary for correctness. When a Ref gets dropped, it tells the RefCell that it's not borrowed anymore. So if did manage to hold onto our reference longer than the Ref existed, we could get a RefMut while a reference was kicking around and totally break Rust's type system in half.

So where does that leave us? We only want to return a reference, but we need to keep this Ref thing around. But as soon as we return the reference from peek, the function is over and the Ref goes out of scope.

😖

As far as I know, we're actually totally dead in the water here. You can't totally encapsulate the use of RefCells like that.

But... what if we just give up on totally hiding our implementation details? What if we returns Refs?

pub fn peek_front(&self) -> Option<Ref<T>> {
    self.head.as_ref().map(|node| {
        node.borrow()
    })
}
> cargo build
   Compiling lists v0.1.0 (file:///Users/ABeingessner/dev/too-many-lists/lists)
src/fourth.rs:62:40: 62:46 error: use of undeclared type name `Ref` [E0412]
src/fourth.rs:62     pub fn peek_front(&self) -> Option<Ref<T>> {
                                                        ^~~~~~
error: aborting due to previous error
Could not compile `lists`.

Blurp. Gotta import some stuff.

use std::cell::{Ref, RefMut, RefCell};
> cargo build
   Compiling lists v0.1.0 (file:///Users/ABeingessner/dev/too-many-lists/lists)
src/fourth.rs:63:9: 65:11 error: mismatched types:
 expected `core::option::Option<core::cell::Ref<'_, T>>`,
    found `core::option::Option<core::cell::Ref<'_, fourth::Node<T>>>`
(expected type parameter,
    found struct `fourth::Node`) [E0308]
src/fourth.rs:63         self.head.as_ref().map(|node| {
src/fourth.rs:64             node.borrow()
src/fourth.rs:65         })
src/fourth.rs:63:9: 65:11 help: run `rustc --explain E0308` to see a detailed explanation
error: aborting due to previous error
Could not compile `lists`.

Hmm... that's right. We have a Ref<Node<T>>, but we want a Ref<T>. We could abandon all hope of encapsulation and just return that. We could also make things even more complicated and wrap Ref<Node<T>> in a new type to only expose access to an &T.

Both of those options are kinda lame.

Instead, we're going to go deeper down the nightly unstable features hole. This one is actually gratuitous since the newtype solution is actually fine. But we're already on nightly, and this list already has me deeply depressed. Let's have some fun. Our source of fun is this beast:

map<U, F>(orig: Ref<'b, T>, f: F) -> Ref<'b, U>
    where F: FnOnce(&T) -> &U,
          U: ?Sized

Make a new Ref for a component of the borrowed data.

Yes: just like you can map over an Option, you can map over a Ref.

I'm sure someone somewhere is really excited because monads or whatever but I don't care about any of that. Also I don't think it's a proper monad since there's no None-like case. But I digress.

It's cool and that's all that matters to me. I need this.

pub fn peek_front(&self) -> Option<Ref<T>> {
    self.head.as_ref().map(|node| {
        Ref::map(node.borrow(), |node| &node.elem)
    })
}
> cargo build
   Compiling lists v0.1.0 (file:///Users/ABeingessner/dev/too-many-lists/lists)
src/fourth.rs:64:13: 64:21 error: use of unstable library feature 'cell_extras': recently added
src/fourth.rs:64             Ref::map(node.borrow(), |node| &node.elem)
                             ^~~~~~~~
note: in expansion of closure expansion
src/fourth.rs:63:32: 65:10 note: expansion site
src/fourth.rs:64:13: 64:21 help: add #![feature(cell_extras)] to the crate attributes to enable
src/fourth.rs:1:22: 1:28 warning: unused import, #[warn(unused_imports)] on by default
src/fourth.rs:1 use std::cell::{Ref, RefMut, RefCell};
                                     ^~~~~~
error: aborting due to previous error
Could not compile `lists`.

Yeah Yeah...

// in lib.rs
#![feature(rc_unique, cell_extras)]
> cargo build
   Compiling lists v0.1.0 (file:///Users/ABeingessner/dev/too-many-lists/lists)
src/fourth.rs:1:22: 1:28 warning: unused import, #[warn(unused_imports)] on by default
src/fourth.rs:1 use std::cell::{Ref, RefMut, RefCell};
                                     ^~~~~~

Awww yissss

Let's make sure this is working by munging up the test from our stack. We need to do some munging to deal with the fact that Refs don't implement comparisons.

#[test]
fn peek() {
    let mut list = List::new();
    assert!(list.peek_front().is_none());
    list.push_front(1); list.push_front(2); list.push_front(3);

    assert_eq!(&*list.peek_front().unwrap(), &3);
}
> cargo test
   Compiling lists v0.1.0 (file:///Users/ABeingessner/dev/too-many-lists/lists)
src/fourth.rs:1:22: 1:28 warning: unused import, #[warn(unused_imports)] on by default
src/fourth.rs:1 use std::cell::{Ref, RefMut, RefCell};
                                     ^~~~~~
     Running target/debug/lists-5c71138492ad4b4a

running 10 tests
test first::test::basics ... ok
test fourth::test::basics ... ok
test second::test::basics ... ok
test fourth::test::peek ... ok
test second::test::iter_mut ... ok
test second::test::into_iter ... ok
test third::test::basics ... ok
test second::test::peek ... ok
test second::test::iter ... ok
test third::test::iter ... ok

test result: ok. 10 passed; 0 failed; 0 ignored; 0 measured

   Doc-tests lists

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured

Great!

results matching ""

    No results matching ""