Expand description

Example of restricting statically-checked mutability for types, that are not originally designed for this (e.g. FFI wrappers etc) or which uses interior mutability, but still need verbose API interface.

The problem

Let’s start from the monkey_ffi module.

This is an example of extern C API, that looks like object-oriented, but is not safe and not guarantees the existence of objects, as well as their relations.

It looks like tree-structure:

Root
----Window
----Frame
----|----FrameButton
----WindowButton

Ideally, only one object at time should be mutable, and we have to guarantee, that parent lives at least as long as child.

But the problem is, that with Rc, or just pointers, on every fold level parent mutability state is lost. We can not make easily two versions of Window: one, that keeps &'a mut Root and other, that keeps &'a Root, just because we will need to type whole the implementation block twice.

But the fact, that Self, &Self and &mut Self are complete different types and this discussion pointed me the solution.

Actually, mutability in rust is tri-state: immutable, mutable, and that, which we don’t care about. Which we want to be «generic». So, let’s start from declaring types, that will represent all the three states: two concrete structs and one trait.

trait ProbablyMutable {
fn is_mutable(&self) -> bool;
}
struct Mutable;
impl ProbablyMutable for Mutable {
    fn is_mutable(&self) -> bool {
        true
    }
}
struct Immutable;
impl ProbablyMutable for Immutable {
    fn is_mutable(&self) -> bool {
        false
    }
}

Then we will use these types as markers for future parametrization.

Now let’s make a skeleton of object structure and ensure that no child will outlive their parents.

struct Root;

Here we use PhantomData to keep «generic» part outside of any concrete parent object. Later, it will help not to have in a deep children structure scary constructions, like: SecondChild<&mut Parent, &mut FirstChild<&mut Parent>>

struct Window<'a, T: ProbablyMutable> {
    id: usize,
    name: String,
    frames_amount: usize,
    buttons_amount: usize,
    root: &'a Root,
    mutability: PhantomData<T>,
}

struct Frame<'a, T: ProbablyMutable> {
    window: &'a Window<'a, T>,
    id: usize,
    width_px: Option<u16>,
    buttons_amount: usize,
}

struct WindowButton<'a, T: ProbablyMutable> {
    id: usize,
    text: String,
    parent: &'a Window<'a, T>,
}

struct FrameButton<'a, T: ProbablyMutable> {
    id: usize,
    text: String,
    parent: &'a Frame<'a, T>,
}

since there are two different functions sets for buttons I decided to consider them as different classes with the single interface (trait) Button.

Theoretically, it should be possible to make a single struct, that keeps differs of types in enum. But for the moment, it seemed to me like an overhead.

Considering the generic mutation, I implement it like implementation of three different types:

  • struct<T: ProbablyMutable> for functions, that should be generic in their mutability.
  • struct<Mutable> for functions, that require object to be mutable.
  • struct<Immutable> for functions, that require object to be immutable.
impl<'a, T: ProbablyMutable> Window<'a, T> {
    fn new(root: &'a Root, id: usize) -> Option<Self> {todo!()}
    fn get_id(&self) -> usize {todo!()}
    fn get_name(&self) -> &String {todo!()}
    fn get_width(&self) -> u16 {todo!()}
}
impl<'a> Window<'a, Immutable> {
    fn get_frame(&self, id: usize) -> Option<Frame<Immutable>> {todo!()}
    fn get_button(&self, id: usize) -> Option<WindowButton<Immutable>> {todo!()}
}
impl<'a> Window<'a, Mutable> {
    fn set_name(&mut self, name: impl Into<String>) {todo!()}
    fn make_frame(&mut self) -> Frame<Mutable> {todo!()}
    fn make_button(&mut self) -> WindowButton<Mutable> {todo!()}
}

For the buttons it look the same, just there will be one shared trait, that has associated type of parent.

trait Button<T: ProbablyMutable>
where
    Self: Sized,
{
    type Parent;
    fn new(parent: Self::Parent, id: usize) -> Option<Self>;
    fn get_id(&self) -> usize;
    fn is_clicked(&self) -> bool;
    fn get_text(&self) -> &String;
}
trait ButtonMut
where
    Self: Sized,
{
    type Parent;
    fn click(&mut self);
    fn set_text(&mut self, text: impl Into<String>);
}
struct WindowButton<'a, T: ProbablyMutable> {
    id: usize,
    text: String,
    parent: &'a Window<'a, T>,
}
impl<'a, T: ProbablyMutable> Button<T> for WindowButton<'a, T> {
    type Parent = &'a Window<'a, T>;
    fn new(parent: Self::Parent, id: usize) -> Option<Self>;
    fn get_id(&self) -> usize;
    fn is_clicked(&self) -> bool;
    fn get_text(&self) -> &String;
}
impl<'a> ButtonMut for WindowButton<'a, Mutable> {
    type Parent = Window<'a, Mutable>;
    fn click(&mut self);
    fn set_text(&mut self, text: impl Into<String>);
}

struct FrameButton<'a, T: ProbablyMutable> {
    id: usize,
    text: String,
    parent: &'a Frame<'a, T>,
}
impl<'a, T: ProbablyMutable> Button<T> for FrameButton<'a, T> {
    type Parent = &'a Frame<'a, T>;
    fn new(parent: Self::Parent, id: usize) -> Option<Self>;
    fn get_id(&self) -> usize;
    fn is_clicked(&self) -> bool;
    fn get_text(&self) -> &String;
}
impl<'a> ButtonMut for FrameButton<'a, Mutable> {
    type Parent = Frame<'a, Mutable>;
    fn click(&mut self) ;
    fn set_text(&mut self, text: impl Into<String>);
}

All the rest is just a bit of boilerplate. You can surf the entire implementation later.

Let’s try to play around:

At first, let’s get our root and make sure we will see an output. For the file rust env RUST_LOG=debug should be set.

env_logger::init();
let mut root = Root::new();
let window1: Window<Mutable> = root.make_child();

Looks good. Adding a window mutates our root, so, window1 is also mut. Let’s add one more!

let window2 = root.make_child();

Guh!: Err: cannot borrow root as mutable more than once at a time. But, generally, this is how it should look like.

Let’s drop our window, but keep its id to get it later.

let w1_id: usize = window1.get_id();
debug!("{}", w1_id);
drop(window1);

Now root is immutable again.

Let’s make sure it is and get 2 windows:

let id2: usize = root.make_child().get_id();
let window1: Window<Immutable> = root.get_child(w1_id).unwrap();
let _window2: Window<Immutable> = root.get_child(id2).unwrap(); // OK!

Interesting, now they are <Immutable>. So, if we try to mutate it, compiler will error:

window1.make_button();
//
Err: no method named `make_button` found for struct `Window<'_,
test::Immutable>` in the current scope. The method was found for
`Window<'a, test::Mutable>`

Continuing:

let mut window1: Window<Mutable> = root.get_child_mut(w1_id).unwrap();
let button: WindowButton<Mutable> = window1.make_button();
let b_id: usize = button.get_id();
// button is dropped.
let mut frame: Frame<Mutable> = window1.make_frame();
let fr_b_id: usize = frame.make_button().get_id();
let f_id: usize = frame.get_id();
// frame is dropped.
debug!("button text: {}", button.get_text());
//
Err: cannot borrow `window1` as mutable more than once at a time

Yes, because button was WindowButton. But, can we borrow button as immutable?

let button: WindowButton<Immutable> = window1.get_button(b_id);
Err: no method named `get_button` found for struct `Window<'_, test::Mutable>`
in the current scope the method was found for - `Window<'a, test::Immutable>`

Now, check that multiple immutable borrows live together:

let window1: Window<Immutable> = root.get_child(w1_id).unwrap();
let frame: Frame<Immutable> = window1.get_frame(f_id).unwrap();
let w_b: WindowButton<Immutable> = window1.get_button(b_id).unwrap();
let fr_b: FrameButton<Immutable> = frame.get_button(fr_b_id).unwrap();

debug!("is window button clicked: {}", w_b.is_clicked());
debug!("is frame button clicked: {}", fr_b.is_clicked());

See full module at GitHub! These docs are available at GitHub pages.

Modules

monkey_ffi 🔒
module that imitates some FFI functions set.

Structs

Frame 🔒
Immutable 🔒
Mutable 🔒
Root 🔒
Window 🔒

Traits

Button 🔒
ButtonMut 🔒

Functions

main 🔒