duplix

An RC-friendly reactive graph with lazy computation and bounded retention.

reactive
signals
incremental
memoization
RC
moon add Yoorkin/duplix@0.2.9
Download zip
Author
Version
0.2.9
License
Apache-2.0
Last updated
last month
Downloads
36

Dependencies

README

#Duplix

Note: This module is experimental.

Duplix is a reactive graph built from two paired strands: value dependencies and dirty propagation.

It aims to provide bounded retention and does not require manual disposal to avoid memory leak, especially in RC runtime. It also provides lazy computation, cutoff propagation, and at-most-once recomputation per node after each update.

#Core idea

parent <--derived--- child1 <--derived--- child2

For a reactive graph child2 -> child1 -> parent, Duplix splits the graph into two strands:

  • Dirty propagation: propagates dirty flags after values change.
  • Value dependencies: update node values when nodes are read.

Initially, the whole graph is dirty, so the dirty-propagation edges are not built yet:

Node(parent) --ref-> Node(child1) --ref-> Node(child2) | | | ref ref ref | | | v v v Dirty(parent) Dirty(child1) Dirty(child2)

When the user reads parent, Duplix recomputes the value through the value dependencies and builds the dirty-propagation edges for the next update:

Node(parent) --ref-> Node(child1) --ref-> Node(child2) | | | ref ref ref | | | v v v Dirty(parent) <-ref-- Dirty(child1) <-ref-- Dirty(child2)

The next time child2 changes, Duplix marks the related dirty nodes and cuts the dirty-propagation edges. This brings the graph back to the initial dirty state, where the edges can be rebuilt on demand by the next read.

At no point does the value graph contain a reference cycle.

#Testing

The optional Yoorkin/duplix/testing package provides a lazy Probe for counting recomputations in black-box tests. Import it with for "test" so it does not become a runtime dependency of the package under test.

#
Enumerate

pub(open) trait Enumerate {
fn tag(Self) -> String
}

#
DirtyFlag

type DirtyFlag

DirtyFlag(parent) <--ref--- DirtyFlag(child)
impl Eq for DirtyFlag

#
Node

type Node[T]

child ---derived---> parent Node(parent) ---ref---> Node(child) | | ref ref | | v v DirtyFlag(parent) <---ref--- DirtyFlag(child)
impl Compare for Node[T]
impl Eq for Node[T]

#
Node::assoc

fn[K : Hash + Eq, V : Eq, C : Eq] Node::assoc(a : Node[Map[K, V]], f : (K, Node[V]) -> Node[C]) -> Node[Array[C]]

node(dict) ---+ |
node(v1) --?-> node(c1) --+ |---> node(array(c)) node(v2) --?-> node(c2) --+ ... | node(vN) --?-> node(cN) --+

#
Node::bind

fn[A : Eq, B : Eq] Node::bind(a : Node[A], f : (A) -> Node[B]) -> Node[B]

a -----+ |----> output
b1 or b2 ----+

#
Node::enumerate

fn[E : Enumerate + Eq, A : Eq] Node::enumerate(map : Node[E], f : (E) -> Node[A]) -> Node[A]

#
Node::map

fn[A : Eq, B] Node::map(a : Node[A], f : (A) -> B) -> Node[B]

child_a ---derived---> parent_b

#
Node::map1

fn[A : Eq, B] Node::map1(a : Node[A], f : (A) -> B) -> Node[B]

#
Node::map10

fn[A : Eq, B : Eq, C : Eq, D : Eq, E : Eq, F : Eq, G : Eq, H : Eq, I : Eq, J : Eq, K] Node::map10(a : Node[A], b : Node[B], c : Node[C], d : Node[D], e : Node[E], f : Node[F], g : Node[G], h : Node[H], i : Node[I], j : Node[J], k : (A, B, C, D, E, F, G, H, I, J) -> K) -> Node[K]

#
Node::map11

fn[A : Eq, B : Eq, C : Eq, D : Eq, E : Eq, F : Eq, G : Eq, H : Eq, I : Eq, J : Eq, K : Eq, L] Node::map11(a : Node[A], b : Node[B], c : Node[C], d : Node[D], e : Node[E], f : Node[F], g : Node[G], h : Node[H], i : Node[I], j : Node[J], k : Node[K], l : (A, B, C, D, E, F, G, H, I, J, K) -> L) -> Node[L]

#
Node::map12

fn[A : Eq, B : Eq, C : Eq, D : Eq, E : Eq, F : Eq, G : Eq, H : Eq, I : Eq, J : Eq, K : Eq, L : Eq, M] Node::map12(a : Node[A], b : Node[B], c : Node[C], d : Node[D], e : Node[E], f : Node[F], g : Node[G], h : Node[H], i : Node[I], j : Node[J], k : Node[K], l : Node[L], m : (A, B, C, D, E, F, G, H, I, J, K, L) -> M) -> Node[M]

#
Node::map2

fn[A : Eq, B : Eq, C] Node::map2(a : Node[A], b : Node[B], f : (A, B) -> C) -> Node[C]

#
Node::map3

fn[A : Eq, B : Eq, C : Eq, D] Node::map3(a : Node[A], b : Node[B], c : Node[C], f : (A, B, C) -> D) -> Node[D]

#
Node::map4

fn[A : Eq, B : Eq, C : Eq, D : Eq, E] Node::map4(a : Node[A], b : Node[B], c : Node[C], d : Node[D], f : (A, B, C, D) -> E) -> Node[E]

#
Node::map5

fn[A : Eq, B : Eq, C : Eq, D : Eq, E : Eq, F] Node::map5(a : Node[A], b : Node[B], c : Node[C], d : Node[D], e : Node[E], f : (A, B, C, D, E) -> F) -> Node[F]

#
Node::map6

fn[A : Eq, B : Eq, C : Eq, D : Eq, E : Eq, F : Eq, G] Node::map6(a : Node[A], b : Node[B], c : Node[C], d : Node[D], e : Node[E], f : Node[F], g : (A, B, C, D, E, F) -> G) -> Node[G]

#
Node::map7

fn[A : Eq, B : Eq, C : Eq, D : Eq, E : Eq, F : Eq, G : Eq, H] Node::map7(a : Node[A], b : Node[B], c : Node[C], d : Node[D], e : Node[E], f : Node[F], g : Node[G], h : (A, B, C, D, E, F, G) -> H) -> Node[H]

#
Node::map8

fn[A : Eq, B : Eq, C : Eq, D : Eq, E : Eq, F : Eq, G : Eq, H : Eq, I] Node::map8(a : Node[A], b : Node[B], c : Node[C], d : Node[D], e : Node[E], f : Node[F], g : Node[G], h : Node[H], i : (A, B, C, D, E, F, G, H) -> I) -> Node[I]

#
Node::map9

fn[A : Eq, B : Eq, C : Eq, D : Eq, E : Eq, F : Eq, G : Eq, H : Eq, I : Eq, J] Node::map9(a : Node[A], b : Node[B], c : Node[C], d : Node[D], e : Node[E], f : Node[F], g : Node[G], h : Node[H], i : Node[I], j : (A, B, C, D, E, F, G, H, I) -> J) -> Node[J]

#
Node::read

fn[A : Eq] Node::read(a : Node[A]) -> A

#
Node::switch

fn[E : Enumerate + Eq, A : Eq] Node::switch(a : Node[E], f : (E) -> Node[A]) -> Node[A]

#
Node::zip10

fn[A : Eq, B : Eq, C : Eq, D : Eq, E : Eq, F : Eq, G : Eq, H : Eq, I : Eq, J : Eq] Node::zip10(a : Node[A], b : Node[B], c : Node[C], d : Node[D], e : Node[E], f : Node[F], g : Node[G], h : Node[H], i : Node[I], j : Node[J]) -> Node[(A, B, C, D, E, F, G, H, I, J)]

#
Node::zip11

fn[A : Eq, B : Eq, C : Eq, D : Eq, E : Eq, F : Eq, G : Eq, H : Eq, I : Eq, J : Eq, K : Eq] Node::zip11(a : Node[A], b : Node[B], c : Node[C], d : Node[D], e : Node[E], f : Node[F], g : Node[G], h : Node[H], i : Node[I], j : Node[J], k : Node[K]) -> Node[(A, B, C, D, E, F, G, H, I, J, K)]

#
Node::zip12

fn[A : Eq, B : Eq, C : Eq, D : Eq, E : Eq, F : Eq, G : Eq, H : Eq, I : Eq, J : Eq, K : Eq, L : Eq] Node::zip12(a : Node[A], b : Node[B], c : Node[C], d : Node[D], e : Node[E], f : Node[F], g : Node[G], h : Node[H], i : Node[I], j : Node[J], k : Node[K], l : Node[L]) -> Node[(A, B, C, D, E, F, G, H, I, J, K, L)]

#
Node::zip2

fn[A : Eq, B : Eq] Node::zip2(a : Node[A], b : Node[B]) -> Node[(A, B)]

#
Node::zip3

fn[A : Eq, B : Eq, C : Eq] Node::zip3(a : Node[A], b : Node[B], c : Node[C]) -> Node[(A, B, C)]

#
Node::zip4

fn[A : Eq, B : Eq, C : Eq, D : Eq] Node::zip4(a : Node[A], b : Node[B], c : Node[C], d : Node[D]) -> Node[(A, B, C, D)]

#
Node::zip5

fn[A : Eq, B : Eq, C : Eq, D : Eq, E : Eq] Node::zip5(a : Node[A], b : Node[B], c : Node[C], d : Node[D], e : Node[E]) -> Node[(A, B, C, D, E)]

#
Node::zip6

fn[A : Eq, B : Eq, C : Eq, D : Eq, E : Eq, F : Eq] Node::zip6(a : Node[A], b : Node[B], c : Node[C], d : Node[D], e : Node[E], f : Node[F]) -> Node[(A, B, C, D, E, F)]

#
Node::zip7

fn[A : Eq, B : Eq, C : Eq, D : Eq, E : Eq, F : Eq, G : Eq] Node::zip7(a : Node[A], b : Node[B], c : Node[C], d : Node[D], e : Node[E], f : Node[F], g : Node[G]) -> Node[(A, B, C, D, E, F, G)]

#
Node::zip8

fn[A : Eq, B : Eq, C : Eq, D : Eq, E : Eq, F : Eq, G : Eq, H : Eq] Node::zip8(a : Node[A], b : Node[B], c : Node[C], d : Node[D], e : Node[E], f : Node[F], g : Node[G], h : Node[H]) -> Node[(A, B, C, D, E, F, G, H)]

#
Node::zip9

fn[A : Eq, B : Eq, C : Eq, D : Eq, E : Eq, F : Eq, G : Eq, H : Eq, I : Eq] Node::zip9(a : Node[A], b : Node[B], c : Node[C], d : Node[D], e : Node[E], f : Node[F], g : Node[G], h : Node[H], i : Node[I]) -> Node[(A, B, C, D, E, F, G, H, I)]

#
cleanup

fn cleanup() -> Unit

#
constant

fn[T] constant(x : T) -> Node[T]

#
input

fn[T : Eq] input(x : T) -> (Node[T], (T) -> Unit)

#
on_cleanup

fn on_cleanup(f : () -> Unit) -> Unit

Powered by MoonBit

Site sourceReport issuePackagesBuild queueSkillsStatistics

© 2026 mooncakes.io