Skip to content

Instantly share code, notes, and snippets.

@storycraft
Last active March 25, 2025 06:28
Show Gist options
  • Save storycraft/c14086742c5bb6fc3ca64a887aa075f4 to your computer and use it in GitHub Desktop.
Save storycraft/c14086742c5bb6fc3ca64a887aa075f4 to your computer and use it in GitHub Desktop.
A high performance, ergonomic reactivity system with async and `Forget` trait

Rust language is the only compiled language without gc with guaranteed memory safety. Using new Forget(must drop) marker trait, I propose safe, performant, ergonomic event and fine grained reactivity system that solves these problems. Which only work in rust due to rust's unique properties.

Unforgettable types (or must drop types)

Forget or Leak trait is a marker trait for types cannot be forgotten (must run destructor). Rust doesn't have these traits now, but proposed as a solution for scoped task trilemma.

There is a good Forget marker trait RFC. Please read this before proceed.

Borrow checker friendly event system

Let's say we have drop guarantee in rust. We can have safe collection type where inserted items' lifetimes are smaller than the collection using drop guarantee. Then we can have a safe, borrow check friendly event system where listeners are non 'static and detach itself on drop (safety requirement) like

let target = pin!(<EventTarget!(&mut i32)>::new());
{
    let mut b = 2;
    // listener borrows local variable `b`
    let listener = pin!(Listener::new(|a: &mut i32| {
        dbg!(*a + b);
        *a += 1;
        b += 2;
    }));
    // Bind listener
    target.bind(listener);
    // Emit event (*a + b = 7 is printed)
    target.emit(&mut 5);
    // Listener drops before exiting scope, removed from `target`
}

// Emit event but nothing happens as there are no listeners.
target.emit(&mut 0);

Note

Due to lack of Unforgettable types, they are not completely safe (although there are no unsafe blocks in example).

Fine grained reactivity

With these event system, we can create high performance reactivity system on top of it. Please read about the prototype before proceed from here.

let a = pin!(State::new(0));
let b = pin!(State::new(0));

let_effect!({
    b.set(a.get($));
    println!("b is updated because a is changed");
});

let_effect is a macro for creating a new effect. The effect run whenever the local variable a changes. We calculate and place number of required bindings in the effect using $ sign. Notice the block inside let_effect is closure. Because we can use local references, it is also very easy to split long functions into separated.

Async Component

Okay, so we have reactivity and event system. But they all drop on the end of the scope! To solve the problems we need self referential type, but how?

We already have safe self referential in rust.

Self referential in rust:

// fut is self referential
let fut = async {
    let a = 1;
    let b = &b;
    async {}.await;
    // a is captured due to await
    // b is a reference but also captured!
    dbg!(b);
};

Inspired by AsyncUI, we can create gui component combined with event and reactivity system using async.

async fn main() {
    // run application with a root component
    run(container).await;
}

struct Color {
    r: u8,
    g: u8,
    b: u8
 }
 
 // Container component with two colored rect
 async fn container(ui: Ui) {
    let counter = pin!(State::new(0));
    let color1 = pin!(State::new(Color {
        r: 255,
        g: 255,
        b: 255,
    }));
    let color2 = pin!(State::new(Color {
        r: 0,
        g: 0,
        b: 255,
    }));

    let_effect!({
        println!("counter is updated to {}", counter.get($));
    });
 
    // use Future combinator
    join!(
        colored_rect(
            color1,
            pin!(Listener::new(|| counter.update(|prev| prev + 1)),
        ).show(ui.clone()),
        colored_rect(
            color2,
            pin!(Listener::new(|| counter.update(|prev| prev - 1)),
        ).show(ui),
    );
 }
 
 // custom sized rect component
fn colored_rect(
    color: Pin<&State<Color>>,
    on_click: Pin<&Listener<impl FnMut()>>,
) -> impl Component<Output = ()> {
    async |ui| {
        let size = pin!(State::new((50, 50));
        Rect::builder()
            .size(size)
            .color(color)
            .on_click(on_click)
            .build()
            .show(ui).await
    }
}

Output:

image

Remaining questions

  1. Using async blocks and closures remove a lot of boilerplates of declaring self referential structs. But still involves a lot of interior mutability. Is it possible to remove them while keeping its convenience?
  2. Pin projection boilerplates and ergonomics. rust-lang/rust#130494
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment