Created
February 2, 2023 04:04
-
-
Save mendes5/f80dadf823bd4ead46fa20d57ed29d75 to your computer and use it in GitHub Desktop.
Can reactive declarative programming in rust be easier?
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#![feature(local_key_cell_methods)] | |
use std::{cell::RefCell, collections::HashMap}; | |
// Core idea: Rust lets you do this: | |
fn create_closure(initial: u32) -> Box<dyn FnMut()> { | |
let mut captured = initial; | |
Box::new(move || { | |
captured += 1; | |
println!("Now captured is {}", captured); | |
}) | |
} | |
// This is basically a closure allocator | |
// you can now call it multiple times | |
fn idea() { | |
let mut my_closure = create_closure(5); | |
my_closure(); // prints captured is 6 | |
my_closure(); // prints captured is 7 | |
my_closure(); // prints captured is 8 | |
my_closure(); // prints captured is 9 | |
my_closure(); // prints captured is 10 | |
} | |
// which might easier to write than | |
/** | |
* struct Closure { | |
* captured: u32, | |
* } | |
* | |
* impl Closure { | |
* fn new(initial: u32) -> Self { | |
* Self { | |
* captured: initial, | |
* } | |
* } | |
* fn update(&mut self) { | |
* captured += 1; | |
* | |
* println!("Now captured is {}", captured); | |
* } | |
* } | |
* | |
* let mut my_closure = Closure::new(0); | |
* my_closure.update(); | |
* | |
* Since you only need one funny function returning a closure. | |
*/ | |
// Memo: | |
// * A heap allocated closure | |
// * That can be run more than once (it caputres its enviroment muttably) | |
// * Which returns a flag (to decide if the children should be rendered) | |
// * And another heap allocated closure | |
// Double closure is required since trying to call `mount_closure` | |
// on the same closure would panic due to consecutive borrows on the Tree's RefCell | |
type Closure = Box<dyn FnMut() -> (bool, Box<dyn FnMut()>)>; | |
thread_local! { | |
static LAST_ID: RefCell<u64> = RefCell::new(0); | |
// Owns pointers to the closures in the heap | |
// so we can update them again | |
static TREE: RefCell<HashMap<u64, Closure>> = RefCell::new(HashMap::new()); | |
} | |
struct Props { | |
current: u32, | |
max: u32, | |
} | |
// Recursive component | |
fn component_example(props: Props) -> Closure { | |
// Hooks/State/Context/Props | |
let a = props.current; | |
Box::new(move || { | |
// Effects | |
println!("{} Im pringing {}", String::from(" ").repeat(a as usize), a); | |
( | |
true, | |
Box::new(move || { | |
// Children | |
if a < props.max { | |
mount_closure(component_example, Props { current: a + 1, max: props.max }); | |
} | |
}), | |
) | |
}) | |
} | |
// Similar to React.createElement | |
// receives component and props | |
// but dont actually executes them | |
fn mount_closure<T, A: Fn(T) -> Closure>( | |
component: A, | |
props: T, | |
) { | |
let next_id = LAST_ID.with_borrow_mut(|last| { | |
let next = *last; | |
*last += 1; | |
next | |
}); | |
// Need a way to track unmounts | |
// and to reconcile it to an actual tree | |
// instead of a vec | |
TREE | |
// Do our mutation on TREE for this component | |
.with_borrow_mut(|cache| { | |
cache.insert(next_id, component(props)); | |
cache.get_mut(&next_id).map(|f| f()) | |
}) | |
// Release the borrow then mount the children | |
.map(|(can_render, mut mounter)| { | |
if can_render { | |
mounter(); | |
} | |
}); | |
} | |
// What setState would do | |
// but to all components reacting to a given state or context | |
fn update_closure(id: u64) { | |
TREE.with_borrow_mut(|cache| { | |
if let Some(closure) = cache.get_mut(&id) { | |
let (can_render, mounter) = closure(); | |
if can_render { | |
return Some(mounter); | |
} | |
} | |
None | |
}).map(|mut mount| { | |
mount(); | |
}); | |
} | |
fn main() { | |
idea(); | |
// Congratulations sir, you just invented a slower stack: | |
// * Three heap allocations | |
// * Dynamic dispatch to next functions | |
// * Pointer indirections | |
mount_closure(component_example, Props { current: 0, max: 30 }); | |
// But now all memory related to the computations are still stored on | |
// the heap. And i can choose to update a specifc closure (or component) | |
// and all closures that it mounts | |
// | |
// this might come in handy if we have something | |
// like use_state or use_context in the future | |
update_closure(15); | |
// The cost should be about the same of implementatinos using | |
// `struct ClosureX {...}; impl Component for ClosureX { mount/unmont/etc }` | |
// Since creating a `FnMut()` closures are basically an | |
// struct + associated function. Also in the example `ClosureX` would | |
// need to be `dyn` which means `Box<dyn Component>`, so it | |
// would need to be heap allocated anyways if we hope to interact | |
// with it again after the first call. | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment