use std::cell::RefCell;
use std::collections::HashMap;
use std::hash::Hash;
use std::rc::Rc;
use crate::Context;
use crate::slot::SlotHandle;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum MaterializationMode {
Eager,
Lazy,
}
impl MaterializationMode {
pub const DEFAULT: MaterializationMode = MaterializationMode::Eager;
#[inline]
pub fn is_lazy(self) -> bool {
matches!(self, MaterializationMode::Lazy)
}
}
impl Default for MaterializationMode {
fn default() -> Self {
MaterializationMode::DEFAULT
}
}
type FactoryFn<K, V> = dyn Fn(&Context, &K) -> V;
pub struct MaterializedFamily<K, V> {
inner: Rc<Inner<K, V>>,
}
struct Inner<K, V> {
slots: RefCell<HashMap<K, SlotHandle<V>>>,
factory: Rc<FactoryFn<K, V>>,
mode: MaterializationMode,
}
impl<K, V> Clone for MaterializedFamily<K, V> {
fn clone(&self) -> Self {
Self {
inner: Rc::clone(&self.inner),
}
}
}
impl<K, V> MaterializedFamily<K, V>
where
K: Eq + Hash + Clone + 'static,
V: PartialEq + Clone + 'static,
{
pub fn new<I, F>(ctx: &Context, mode: MaterializationMode, keys: I, factory: F) -> Self
where
I: IntoIterator<Item = K>,
F: Fn(&Context, &K) -> V + 'static,
{
let family = Self {
inner: Rc::new(Inner {
slots: RefCell::new(HashMap::new()),
factory: Rc::new(factory),
mode,
}),
};
if !mode.is_lazy() {
for key in keys {
family.materialize(ctx, &key);
}
}
family
}
pub fn eager<I, F>(ctx: &Context, keys: I, factory: F) -> Self
where
I: IntoIterator<Item = K>,
F: Fn(&Context, &K) -> V + 'static,
{
Self::new(ctx, MaterializationMode::Eager, keys, factory)
}
pub fn lazy<F>(_ctx: &Context, factory: F) -> Self
where
F: Fn(&Context, &K) -> V + 'static,
{
Self {
inner: Rc::new(Inner {
slots: RefCell::new(HashMap::new()),
factory: Rc::new(factory),
mode: MaterializationMode::Lazy,
}),
}
}
#[inline]
pub fn mode(&self) -> MaterializationMode {
self.inner.mode
}
fn materialize(&self, ctx: &Context, key: &K) -> SlotHandle<V> {
if let Some(handle) = self.inner.slots.borrow().get(key) {
return *handle;
}
let factory = Rc::clone(&self.inner.factory);
let k = key.clone();
let handle = ctx.memo(move |ctx| factory(ctx, &k));
self.inner.slots.borrow_mut().insert(key.clone(), handle);
handle
}
pub fn handle(&self, ctx: &Context, key: &K) -> SlotHandle<V> {
self.materialize(ctx, key)
}
pub fn observe(&self, ctx: &Context, key: &K) -> V {
let handle = self.materialize(ctx, key);
ctx.get(&handle)
}
pub fn is_materialized(&self, key: &K) -> bool {
self.inner.slots.borrow().contains_key(key)
}
pub fn materialized_count(&self) -> usize {
self.inner.slots.borrow().len()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_mode_is_eager() {
assert_eq!(MaterializationMode::default(), MaterializationMode::Eager);
assert_eq!(MaterializationMode::DEFAULT, MaterializationMode::Eager);
assert!(!MaterializationMode::Eager.is_lazy());
assert!(MaterializationMode::Lazy.is_lazy());
}
#[test]
fn eager_materializes_all() {
let ctx = Context::new();
let fam = MaterializedFamily::eager(&ctx, [1u32, 2, 3, 4], |_ctx, k| k * 10);
assert_eq!(fam.mode(), MaterializationMode::Eager);
for k in [1u32, 2, 3, 4] {
assert!(fam.is_materialized(&k), "eager key {k} allocated at build");
}
assert_eq!(fam.materialized_count(), 4);
}
#[test]
fn lazy_defers_slots() {
let ctx = Context::new();
let fam = MaterializedFamily::lazy(&ctx, |_ctx, k: &u32| k * 10);
assert_eq!(fam.mode(), MaterializationMode::Lazy);
assert_eq!(fam.materialized_count(), 0);
assert!(!fam.is_materialized(&2));
assert_eq!(fam.observe(&ctx, &2), 20);
assert!(fam.is_materialized(&2));
assert_eq!(fam.materialized_count(), 1);
assert!(!fam.is_materialized(&3));
}
#[test]
fn eager_lazy_observationally_equivalent() {
let ctx = Context::new();
let factory = |_ctx: &Context, k: &u32| k * 3 + 1;
let keys = [0u32, 1, 2, 5, 9];
let eager = MaterializedFamily::eager(&ctx, keys, factory);
let lazy = MaterializedFamily::lazy(&ctx, factory);
for k in keys {
let expected = k * 3 + 1;
assert_eq!(eager.observe(&ctx, &k), expected);
assert_eq!(lazy.observe(&ctx, &k), expected);
assert_eq!(eager.observe(&ctx, &k), lazy.observe(&ctx, &k));
}
}
#[test]
fn materialize_preserves_observe() {
let ctx = Context::new();
let fam = MaterializedFamily::lazy(&ctx, |_ctx, k: &u32| k * 7);
let before = fam.observe(&ctx, &4); let _ = fam.observe(&ctx, &9);
assert_eq!(fam.observe(&ctx, &4), before);
}
#[test]
fn lazy_present_monotone_subset_of_eager() {
let ctx = Context::new();
let keys = [1u32, 2, 3, 4, 5];
let factory = |_ctx: &Context, k: &u32| k * 2;
let eager = MaterializedFamily::eager(&ctx, keys, factory);
let lazy = MaterializedFamily::lazy(&ctx, factory);
let mut last = 0;
for k in [2u32, 4] {
let _ = lazy.observe(&ctx, &k);
assert!(lazy.materialized_count() >= last, "present set is monotone");
last = lazy.materialized_count();
let _ = lazy.observe(&ctx, &k);
assert_eq!(lazy.materialized_count(), last, "warm read is idempotent");
}
for k in keys {
if lazy.is_materialized(&k) {
assert!(eager.is_materialized(&k), "lazy present ⊆ eager present");
}
}
assert_eq!(eager.materialized_count(), keys.len());
}
#[test]
fn reactivity_is_orthogonal_to_materialization() {
let ctx = Context::new();
let base = ctx.cell(10i32);
let fam = MaterializedFamily::lazy(&ctx, move |ctx, k: &i32| ctx.get_cell(&base) + k);
assert_eq!(fam.observe(&ctx, &1), 11);
ctx.set_cell(&base, 100);
assert_eq!(fam.observe(&ctx, &1), 101);
}
#[test]
fn same_key_same_slot() {
let ctx = Context::new();
let fam = MaterializedFamily::lazy(&ctx, |_ctx, k: &u32| k + 1);
let h1 = fam.handle(&ctx, &7);
let h2 = fam.handle(&ctx, &7);
assert_eq!(h1.id, h2.id);
assert_eq!(fam.materialized_count(), 1);
}
}