diff --git a/.coveragerc b/.coveragerc index 6ffc95e..f4a632f 100644 --- a/.coveragerc +++ b/.coveragerc @@ -12,5 +12,3 @@ omit = examples/* docs/* */__init__.py - - diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c8f5951..475ee9a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -27,14 +27,14 @@ repos: - id: isort args: ["--profile", "black"] - # MyPy - Type checking - - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.18.2 - hooks: - - id: mypy - additional_dependencies: [] - args: [--ignore-missing-imports] - files: ^(fynx|tests)/ + # MyPy - Type checking, disabled for now + # - repo: https://github.com/pre-commit/mirrors-mypy + # rev: v1.18.2 + # hooks: + # - id: mypy + # additional_dependencies: [] + # args: [--ignore-missing-imports] + # files: ^(fynx|tests)/ # General pre-commit hooks - repo: https://github.com/pre-commit/pre-commit-hooks diff --git a/README.md b/README.md index 9edcdb8..be617b7 100644 --- a/README.md +++ b/README.md @@ -65,12 +65,12 @@ class CartStore(Store): price_per_item = observable(10.0) # Define transformation function -def calculate_total(count_and_price): - count, price = count_and_price +def calculate_total(count, price): return count * price # Reactive computation using .then() -total_price = (CartStore.item_count | CartStore.price_per_item).then(calculate_total) +total_price = (CartStore.item_count + CartStore.price_per_item).then(calculate_total) +# total_price = (CartStore.item_count + CartStore.price_per_item) >> calculate_total # Equivalent! def print_total(total): print(f"Cart Total: ${total:.2f}") @@ -101,28 +101,43 @@ The common thread: data flows through transformations, and multiple parts of you This breadth isn't accidental. The universal properties underlying FynX apply to any scenario involving time-varying values and compositional transformations—which describes a surprisingly large fraction of software. + +## The Five Reactive Operators + +FynX provides five composable operators that form a complete algebra for reactive programming. You can use either the symbolic operators (`>>`, `+`, `&`, `|`, `~`) or their natural language method equivalents (`.then()`, `.alongside()`, `.requiring()`, `.either()`, `.negate()`): + +| Operator | Method | Operation | Purpose | Example | +|----------|--------|-----------|---------|---------| +| `>>` | `.then()` | Transform | Apply functions to values | `price >> (lambda p: f"${p:.2f}")` | +| `+` | `.alongside()` | Combine | Merge observables into read-only tuples | `(first + last) >> join` | +| `&` | `.requiring()` | Filter | Gate reactivity based on conditions | `file & valid & ~processing` | +| `\|` | `.either()` | Logical OR | Combine boolean conditions | `is_error \| is_warning` | +| `~` | `.negate()` | Negate | Invert boolean conditions | `~is_loading` | + +Each operation creates a new observable. Chain them to build sophisticated reactive systems from simple parts. These operators correspond to precise mathematical structures—functors, products, pullbacks—that guarantee correct behavior under composition. + ## The Mathematical Guarantee -Here's what makes FynX different: the reactive behavior doesn't just work for the examples you see—it works by mathematical necessity for any reactive program you could construct. FynX is built on solid mathematical foundations from category theory. These aren't abstractions for their own sake—they're implementation principles that guarantee correctness and enable powerful optimizations. +You don't need to understand category theory at all to use FynX, though it is what makes FynX different: the reactive behavior doesn't just work for the examples you see—it works by mathematical necessity for any reactive program you could construct. FynX is built on solid mathematical foundations from category theory. These aren't abstractions for their own sake—they're implementation principles that guarantee correctness and enable powerful optimizations. -FynX satisfies specific universal properties from category theory (covered in the [**Mathematical Foundations**](https://off-by-some.github.io/fynx/generation/markdown/mathematical-foundations/)) These aren't abstractions for their own sake; they're implementation principles that guarantee correctness: +You don't need to understand category theory to use FynX, but it's what makes FynX reliable: the reactive behavior isn't just validated by examples—it's guaranteed by mathematical necessity. Every reactive program you construct will work correctly because FynX is built on universal properties from category theory (detailed in the [**Mathematical Foundations**](https://off-by-some.github.io/fynx/generation/markdown/mathematical-foundations/)). These aren't abstract concepts for their own sake; they're implementation principles that ensure correctness and enable powerful optimizations. -* **Functoriality**: Any function you lift with `>>` preserves composition exactly. Chain transformations freely—the order of operations is guaranteed. -* **Products**: Combining observables with `|` creates proper categorical products. No matter how you nest combinations, the structure remains coherent. -* **Pullbacks**: Filtering with `&` constructs mathematical pullbacks. Stack conditions in any order—the semantics stay consistent. +FynX satisfies specific universal properties from category theory, guaranteeing correctness: +* **Functoriality**: Transformations with `>>` preserve composition. Your chains work exactly as expected, regardless of how you compose them. +* **Products**: Combining observables with `+` creates proper categorical products. No matter how complex your combinations, the structure stays coherent. +* **Pullbacks**: Filtering with `&` constructs mathematical pullbacks. Stack conditions freely—the meaning never changes. -The functoriality property guarantees that lifted functions preserve composition: +The functoriality property guarantees that lifted functions preserve composition: $$ \mathcal{O}(\mathrm{id}) = \mathrm{id} \quad \mathcal{O}(g \circ f) = \mathcal{O}g \circ \mathcal{O}f $$ -You don't need to understand category theory to use FynX. The mathematics works beneath the surface, ensuring that complex reactive systems composed from simple parts behave predictably under all transformations. Write declarative code describing relationships, and the universal properties guarantee those relationships hold. - -These same categorical structures also enable FynX's automatic optimizer—composition laws prove that `obs >> f >> g >> h` can safely fuse into a single operation, product properties allow sharing common computations, and pullback semantics let filters combine without changing meaning. The theory doesn't just ensure correctness; it shows exactly which performance optimizations preserve your program's semantics. +In practice, this means complex reactive systems composed from simple parts behave predictably under all transformations. You describe what relationships should exist; FynX guarantees they hold. -Think of it as a particularly thorough test suite—one that covers not just the cases you thought to write, but every possible case that could theoretically exist (but yes, we've got the ["real deal" tests](./tests/test_readme.py) if you want to live dangerously). +These same categorical structures enable FynX's automatic optimizer. Composition laws prove `obs >> f >> g >> h` can safely fuse into a single operation. Product properties allow sharing common computations. Pullback semantics let filters combine without changing meaning. The theory doesn't just ensure correctness—it shows exactly which optimizations are safe. +Think of it like an impossibly thorough test suite: one covering not just the cases you wrote, but every case that could theoretically exist. (We also ship with [conventional tests](./tests/), naturally.) ## Performance @@ -135,6 +150,7 @@ poetry run python scripts/benchmark.py ``` Below is a sample of the output from the above command: + ``` FynX Benchmark Configuration: TIME_LIMIT_SECONDS: 1.0 @@ -195,24 +211,12 @@ class AppState(Store): AppState.username = "off-by-some" # Normal assignment, reactive behavior ``` -Stores provide structure for related state and enable features like store-level reactions and serialization. With observables established, you compose them using FynX's four fundamental operators. - -## The Four Reactive Operators - -FynX provides four composable operators that form a complete algebra for reactive programming: - -| Operator | Operation | Purpose | Example | -|----------|-----------|---------|---------| -| `>>` | Transform | Apply functions to values | `price >> (lambda p: f"${p:.2f}")` | -| `\|` | Combine | Merge observables into tuples | `(first \| last) >> join` | -| `&` | Filter | Gate based on conditions | `file & valid & ~processing` | -| `~` | Negate | Invert boolean conditions | `~is_loading` | +Stores provide structure for related state and enable features like store-level reactions and serialization. With observables established, you compose them using FynX's five fundamental operators. -Each operation creates a new observable. Chain them to build sophisticated reactive systems from simple parts. These operators correspond to precise mathematical structures—functors, products, pullbacks—that guarantee correct behavior under composition. -## Transforming Data with `>>` +## Transforming Data with `>>` or `.then()` -The `>>` operator transforms observables through functions. Chain multiple transformations to build [derived observables](https://off-by-some.github.io/fynx/generation/markdown/derived-observables/): +The `>>` operator (or `.then()` method) transforms observables through functions. Chain multiple transformations to build [derived observables](https://off-by-some.github.io/fynx/generation/markdown/derived-observables/): ```python # Define transformation functions @@ -240,9 +244,10 @@ result_operator = (counter Each transformation creates a new observable that recalculates when its source changes. This chaining works predictably because `>>` implements functorial mapping—structure preservation under transformation. -## Combining Observables with `|` +## Combining Observables with `+` or `.alongside()` -Use `|` to combine multiple observables into reactive tuples: +Use `+` (or `.alongside()`) to combine multiple observables into reactive tuples. +Merged observables are read-only computed observables that derive their value from their source observables: ```python class User(Store): @@ -250,28 +255,31 @@ class User(Store): last_name = observable("Doe") # Define transformation function -def join_names(first_and_last): - first, last = first_and_last +def join_names(first, last): return f"{first} {last}" # Combine and transform using .then() -full_name_method = (User.first_name | User.last_name).then(join_names) +full_name_method = (User.first_name + User.last_name).then(join_names) # Alternative using >> operator -full_name_operator = (User.first_name | User.last_name) >> join_names +full_name_operator = (User.first_name + User.last_name) >> join_names + +# Merged observables are read-only +merged = User.first_name + User.last_name +# merged.set(("Jane", "Smith")) # Raises ValueError: Computed observables are read-only ``` When any combined observable changes, downstream values recalculate automatically. This operator constructs categorical products, ensuring combination remains symmetric and associative regardless of nesting. -> **Note:** The `|` operator will transition to `@` in a future release to support logical OR operations. +## Filtering with `&`, `.requiring()`, `~`, `.negate()`, `|`, and `.either()` -## Filtering with `&` and `~` - -The `&` operator filters observables to emit only when [conditions](https://off-by-some.github.io/fynx/generation/markdown/conditionals/) are met. Use `~` to negate: +The `&` operator (or `.requiring()`) filters observables to emit only when [conditions](https://off-by-some.github.io/fynx/generation/markdown/conditionals/) are met. Use `~` (or `.negate()`) to invert, and `|` (or `.either()`) for logical OR conditions: ```python uploaded_file = observable(None) is_processing = observable(False) +is_error = observable(False) +is_warning = observable(True) # Define validation function def is_valid_file(f): @@ -281,52 +289,83 @@ def is_valid_file(f): is_valid_method = uploaded_file.then(is_valid_file) is_valid_operator = uploaded_file >> is_valid_file -# Filter using & operator -preview_ready_method = uploaded_file & is_valid_method & (~is_processing) +# Filter using & operator (or .requiring() method) +preview_ready_method = uploaded_file.requiring(is_valid_method).requiring(is_processing.negate()) preview_ready_operator = uploaded_file & is_valid_operator & (~is_processing) + +# Logical OR using | operator (or .either() method) +needs_attention = is_error | is_warning +# Alternative: needs_attention = is_error.either(is_warning) ``` -The `preview_ready` observable emits only when all conditions align—file exists, it's valid, and processing is inactive. This filtering emerges from pullback constructions that create a "smart gate" filtering to the fiber where all conditions are True. +The `preview_ready` observable emits only when all conditions align—file exists, it's valid, and processing is inactive. The `needs_attention` observable emits when any error or warning condition is true. This filtering emerges from pullback constructions that create a "smart gate" filtering to the fiber where all conditions are True. ## Reacting to Changes -React to observable changes using the [`@reactive`](https://off-by-some.github.io/fynx/generation/markdown/using-reactive/) decorator or subscriptions: +React to observable changes using the [`@reactive`](https://off-by-some.github.io/fynx/generation/markdown/using-reactive/) decorator or subscriptions. + +**The fundamental principle**: `@reactive` is for side effects only—UI updates, logging, network calls, and other operations that interact with the outside world. For deriving new values from existing data, use the `>>` operator instead. This separation keeps your reactive system predictable and maintainable. + +**Important note on timing**: Reactive functions don't fire immediately when created—they only fire when their dependencies *change*. This follows from FynX's pullback semantics in category theory. If you need initialization logic, handle it separately before setting up the reaction. ```python from fynx import reactive -# Define reaction functions -def handle_change(value): - print(f"Changed: {value}") +# GOOD: Side effects with @reactive +@reactive(user_count) +def update_dashboard(count): + render_ui(f"Users: {count}") # Side effect: UI update + +@reactive(data_stream) +def sync_to_server(data): + api.post('/sync', data) # Side effect: network I/O -def print_new_value(x): - print(f"New value: {x}") +@reactive(error_log) +def log_errors(error): + print(f"Error: {error}") # Side effect: logging -# Dedicated reaction functions -@reactive(observable) -def handle_change(value): - print(f"Changed: {value}") +# GOOD: Data transformations with >> operator +doubled = count >> (lambda x: x * 2) # Pure transformation +formatted = doubled >> (lambda x: f"${x:.2f}") # Pure transformation -# Inline reactions -observable.subscribe(print_new_value) +# Inline subscriptions for dynamic behavior +observable.subscribe(lambda x: print(f"New value: {x}")) -# Conditional reactions using conditional observables -condition1 = observable(True) -condition2 = observable(False) +# Conditional reactions using boolean operators +is_logged_in = observable(False) +has_data = observable(False) +is_loading = observable(True) -def check_conditions(): - return condition1.value and condition2.value +# React only when logged in AND has data AND NOT loading +@reactive(is_logged_in & has_data & ~is_loading) +def sync_when_ready(should_sync): + if should_sync: + perform_sync() # Side effect: network operation -# Create conditional observable -all_conditions_met = (condition1 | condition2).then(check_conditions) +# Multiple observables via derived state +first_name = observable("Alice") +last_name = observable("Smith") + +# Derive first, then react +full_name = (first_name + last_name) >> (lambda f, l: f"{f} {l}") + +@reactive(full_name) +def update_greeting(name): + display_message(f"Hello, {name}!") # Side effect: UI update +``` + +**Lifecycle management**: Use `.unsubscribe()` to stop reactive behavior when cleaning up components or changing modes. After unsubscribing, the function returns to normal, non-reactive behavior and can be called manually again. + +```python +@reactive(data_stream) +def process_data(data): + handle_data(data) -@reactive(all_conditions_met) -def on_conditions_met(conditions_met): - if conditions_met: - print("All conditions satisfied!") +# Later, during cleanup +process_data.unsubscribe() # Stops reacting to changes ``` -Choose the pattern that fits your context. These reactions fire automatically because the dependency graph tracks relationships through the categorical structure underlying observables. +**Remember**: Use `@reactive` for side effects at your application's boundaries—where your pure reactive data flow meets the outside world. Use `>>`, `+`, `&`, `|`, and `~` for all data transformations and computations. This "functional core, reactive shell" pattern is what makes reactive systems both powerful and maintainable. ## Additional Examples @@ -347,7 +386,7 @@ These examples demonstrate how FynX's composable primitives scale from simple to Deep mathematics should enable simpler code, not complicate it. FynX grounds itself in category theory precisely because those abstractions—functors, products, pullbacks—capture the essence of composition without the accidents of implementation. Users benefit from mathematical rigor whether they recognize the theory or not. -The interface reflects this. Observables feel like ordinary values—read them, write them, pass them around. Reactivity works behind the scenes, tracking dependencies through categorical structure without requiring explicit wiring. Method chaining flows naturally: `observable(42).subscribe(print)` reads as plain description, not ceremony. The `>>` operator transforms, `|` combines, `&` filters—each produces new observables ready for further composition. Complex reactive systems emerge from simple, reusable pieces. +The interface reflects this. Observables feel like ordinary values—read them, write them, pass them around. Reactivity works behind the scenes, tracking dependencies through categorical structure without requiring explicit wiring. Method chaining flows naturally: `observable(42).subscribe(print)` reads as plain description, not ceremony. The `>>` operator transforms, `+` combines, `&` filters, `|` creates OR conditions, `~` negates—each produces new observables ready for further composition. Complex reactive systems emerge from simple, reusable pieces. FynX offers multiple APIs because different contexts call for different styles. Use decorators when conciseness matters, direct calls when you need explicit control, context managers when reactions should be scoped. The library adapts to your preferred way of working. diff --git a/docs/README.md b/docs/README.md index baf47af..bd42bbc 100644 --- a/docs/README.md +++ b/docs/README.md @@ -9,12 +9,14 @@ docs/ ├── README.md # This file ├── index.md # Main landing page (MkDocs compatible) ├── generation/ # Documentation generation files -│ ├── mkdocs.yml # MkDocs configuration -│ ├── markdown/ # Generated markdown documentation -│ │ └── api.md # API reference (processed by mkdocstrings) +│ ├── markdown/ # Documentation source files +│ │ ├── mkdocs.yml # MkDocs configuration +│ │ ├── tutorial/ # Tutorial content +│ │ ├── reference/ # API reference documentation +│ │ └── mathematical/ # Mathematical foundations │ └── scripts/ # Documentation generation scripts │ ├── generate_html.py # HTML documentation generator -│ └── preview_docs.sh # Preview server launcher +│ └── preview_html_docs.sh # Preview server launcher ├── images/ # Universal images and assets │ ├── banner.svg # Main logo/banner for documentation │ ├── fynx_icon.svg # Icon for favicons and logos @@ -34,14 +36,14 @@ python docs/generation/scripts/generate_html.py ### Previewing Documentation ```bash -bash docs/generation/scripts/preview_docs.sh +bash docs/generation/scripts/preview_html_docs.sh ``` This will: 1. Build HTML documentation using MkDocs with mkdocstrings -2. Output to the `site/` directory +2. Start a local development server at http://localhost:8000 ### Adding New Pages -1. Create markdown files directly in the `docs/` directory -2. Add entries to the `nav` section in `docs/build/mkdocs.yml` +1. Create markdown files in the appropriate subdirectory (`tutorial/`, `reference/`, or `mathematical/`) +2. Add entries to the `nav` section in `docs/generation/markdown/mkdocs.yml` diff --git a/docs/generation/markdown/assets/images/banner.svg b/docs/generation/markdown/assets/images/banner.svg new file mode 100644 index 0000000..f03df0a --- /dev/null +++ b/docs/generation/markdown/assets/images/banner.svg @@ -0,0 +1,591 @@ + + + diff --git a/docs/generation/markdown/assets/images/icon_350x350.png b/docs/generation/markdown/assets/images/icon_350x350.png new file mode 100644 index 0000000..6e0b355 Binary files /dev/null and b/docs/generation/markdown/assets/images/icon_350x350.png differ diff --git a/docs/javascripts/mathjax.js b/docs/generation/markdown/assets/javascripts/mathjax.js similarity index 98% rename from docs/javascripts/mathjax.js rename to docs/generation/markdown/assets/javascripts/mathjax.js index 7800bbd..2f5bcd3 100644 --- a/docs/javascripts/mathjax.js +++ b/docs/generation/markdown/assets/javascripts/mathjax.js @@ -13,4 +13,4 @@ window.MathJax = { document$.subscribe(() => { MathJax.typesetPromise?.(); // ensure Promise exists to avoid type errors -}); \ No newline at end of file +}); diff --git a/docs/generation/markdown/assets/stylesheets/extra.css b/docs/generation/markdown/assets/stylesheets/extra.css new file mode 100644 index 0000000..72afd80 --- /dev/null +++ b/docs/generation/markdown/assets/stylesheets/extra.css @@ -0,0 +1,755 @@ +/* ============================================ + OPTIMIZED MKDOCS THEME - Maximum Content Utilization + ============================================ */ + +/* Core Color System - Refined for maximum readability */ +:root { + /* Primary palette */ + --md-primary-fg-color: #0f172a; + --md-primary-fg-color--light: #1e293b; + --md-primary-fg-color--dark: #020617; + + /* Accent colors - vibrant yet professional */ + --md-accent-fg-color: #3b82f6; + --md-accent-fg-color--hover: #2563eb; + --md-accent-fg-color--light: rgba(59, 130, 246, 0.08); + + /* Neutral colors for text hierarchy */ + --text-primary: #0f172a; + --text-secondary: #475569; + --text-tertiary: #94a3b8; + + /* Surface colors */ + --surface-primary: #ffffff; + --surface-secondary: #f8fafc; + --surface-hover: #f1f5f9; + + /* Shadows for depth */ + --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05); + --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1); + --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1); + --shadow-accent: 0 4px 20px rgba(59, 130, 246, 0.25); + + /* Transitions */ + --transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1); + --transition-base: 250ms cubic-bezier(0.4, 0, 0.2, 1); +} + +/* ============================================ + LAYOUT OPTIMIZATION - Maximum Content Area + ============================================ */ + +/* Optimize the main grid to use available space */ +.md-grid { + max-width: none !important; + margin: 0 !important; +} + +/* Make sidebars narrower and more compact */ +.md-sidebar--primary { + width: 14rem !important; + left: 0; +} + +.md-sidebar--secondary { + width: 14rem !important; + right: 0; +} + +/* Optimize sidebar scrollwrap */ +.md-sidebar__scrollwrap { + margin: 0 !important; +} + +.md-sidebar__inner { + padding: 0.6rem 0.8rem !important; +} + +/* Expand content area significantly */ +.md-content { + max-width: none !important; + padding: 0 2.5rem !important; + width: auto !important; + flex-grow: 1 !important; +} + +.md-content__inner { + max-width: none !important; + width: 100% !important; + padding: 1.5rem 0 6rem 0 !important; + margin: 0 auto !important; +} + +/* Override the inline styles on the article/typeset container */ +.md-typeset { + max-width: none !important; + width: 100% !important; +} + +.md-content__inner.md-typeset { + max-width: none !important; + width: 100% !important; +} + +/* Use full width on larger screens */ +@media screen and (min-width: 76.25em) { + .md-content { + width: auto !important; + } + + .md-sidebar--primary { + width: 14rem !important; + } + + .md-sidebar--secondary { + width: 14rem !important; + } + + .md-content__inner { + width: 100% !important; + } +} + +/* Medium screens - balance sidebars and content */ +@media screen and (min-width: 60em) and (max-width: 76.24em) { + .md-sidebar--primary { + width: 12rem !important; + } + + .md-sidebar--secondary { + width: 12rem !important; + } + + .md-content { + width: auto !important; + } + + .md-content__inner { + width: 100% !important; + } +} + +/* Responsive collapse for smaller screens */ +@media screen and (max-width: 76.1875em) { + .md-sidebar--primary { + transform: translateX(-14.5rem) !important; + } + + [data-md-toggle="drawer"]:checked ~ .md-container .md-sidebar--primary { + transform: translateX(0) !important; + } + + .md-content { + margin-left: 0 !important; + width: 100% !important; + } + + .md-content__inner { + width: 100% !important; + } +} + +/* ============================================ + HEADER - Clean and Elegant + ============================================ */ +.md-header { + background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%); + box-shadow: 0 1px 0 0 rgba(255, 255, 255, 0.1); + backdrop-filter: blur(10px); + transition: box-shadow var(--transition-base); + position: sticky; + top: 0; + z-index: 4; +} + +.md-header--shadow { + box-shadow: var(--shadow-md); +} + +.md-header__inner { + padding: 0.4rem 1.5rem; + max-width: none; + margin: 0 auto; +} + +.md-header__button.md-icon { + color: #f8fafc; + opacity: 0.9; + transition: opacity var(--transition-fast), transform var(--transition-fast); +} + +.md-header__button.md-icon:hover { + opacity: 1; + transform: scale(1.05); +} + +/* Header title */ +.md-header__title { + font-weight: 600; + letter-spacing: -0.02em; + flex-grow: 0; +} + +/* ============================================ + NAVIGATION - Compact and Accessible + ============================================ */ + +/* Primary navigation */ +.md-nav--primary .md-nav__link { + color: #e2e8f0; + font-weight: 500; + transition: color var(--transition-fast); +} + +.md-nav--primary .md-nav__link:hover, +.md-nav--primary .md-nav__link:focus { + color: #3b82f6; +} + +/* Top-level nav items only - make them bold */ +.md-nav--primary > .md-nav__list > .md-nav__item > .md-nav__link { + font-weight: 700; +} + +/* Nested navigation - keep normal weight */ +.md-nav__item--nested > .md-nav__link { + font-weight: 600; + color: var(--text-primary) !important; +} + +/* Child items should NOT be bold */ +.md-nav__item--nested .md-nav .md-nav__link { + font-weight: 450 !important; +} + +/* Sidebar navigation - more compact */ +.md-sidebar { + background-color: transparent; +} + +.md-nav { + font-size: 0.75rem; + line-height: 1.4; +} + +.md-nav__title { + font-weight: 700; + font-size: 0.65rem; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--text-tertiary); + padding: 0.5rem 0.6rem 0.3rem; + margin-top: 1rem; +} + +.md-nav__title:first-child { + margin-top: 0; +} + +.md-nav__item { + padding: 0.05rem 0; +} + +.md-nav__link { + color: var(--text-secondary) !important; + padding: 0.4rem 0.6rem; + border-radius: 0.375rem; + font-weight: 450; + transition: all var(--transition-fast); + position: relative; + overflow: hidden; + font-size: 0.8rem; +} + +.md-nav__link::before { + content: ''; + position: absolute; + left: 0; + top: 0; + bottom: 0; + width: 3px; + background: var(--md-accent-fg-color); + transform: scaleY(0); + transition: transform var(--transition-base); + border-radius: 0 2px 2px 0; +} + +.md-nav__link:hover, +.md-nav__link:focus { + background-color: var(--surface-hover) !important; + color: var(--md-accent-fg-color) !important; +} + +.md-nav__link--active { + color: var(--md-accent-fg-color) !important; + background-color: var(--md-accent-fg-color--light) !important; + font-weight: 600; +} + +.md-nav__link--active::before { + transform: scaleY(1); +} + +/* Nested navigation */ +.md-nav__item--nested > .md-nav__link { + font-weight: 600; + color: var(--text-primary) !important; +} + +/* Secondary navigation (TOC) */ +.md-nav--secondary .md-nav__link { + font-size: 0.75rem; + padding: 0.3rem 0.6rem; +} + +/* ============================================ + CONTENT AREA - Optimal Readability with More Space + ============================================ */ + +/* Typography hierarchy - optimized for wider layout */ +.md-content h1 { + font-weight: 800; + font-size: 2.75rem; + line-height: 1.2; + letter-spacing: -0.03em; + margin-bottom: 1.5rem; + color: var(--text-primary); + margin-top: 0; +} + +.md-content h2 { + font-weight: 700; + font-size: 2rem; + line-height: 1.3; + letter-spacing: -0.02em; + margin-top: 3rem; + margin-bottom: 1rem; + color: var(--text-primary); +} + +.md-content h3 { + font-weight: 600; + font-size: 1.5rem; + margin-top: 2rem; + margin-bottom: 0.75rem; + color: var(--text-primary); +} + +.md-content h4 { + font-weight: 600; + font-size: 1.25rem; + margin-top: 1.5rem; + margin-bottom: 0.5rem; + color: var(--text-primary); +} + +.md-content p { + line-height: 1.75; + margin-bottom: 1.25rem; + color: var(--text-secondary); + font-size: 1rem; + max-width: none; +} + +/* Lists with better spacing */ +.md-content ul, +.md-content ol { + margin-bottom: 1.25rem; + padding-left: 1.5rem; +} + +.md-content li { + margin-bottom: 0.5rem; + line-height: 1.7; + color: var(--text-secondary); +} + +/* Links - Subtle but noticeable */ +.md-content a { + color: var(--md-accent-fg-color); + text-decoration: none; + font-weight: 500; + border-bottom: 1px solid transparent; + transition: all var(--transition-fast); + position: relative; +} + +.md-content a:hover, +.md-content a:focus { + color: var(--md-accent-fg-color--hover); + border-bottom-color: var(--md-accent-fg-color--hover); +} + +/* ============================================ + BUTTONS - Modern and Engaging + ============================================ */ +.md-button, +.md-button--primary { + background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%); + color: white; + font-weight: 600; + padding: 0.625rem 1.5rem; + border: none; + border-radius: 0.5rem; + box-shadow: var(--shadow-sm); + transition: all var(--transition-base); + cursor: pointer; +} + +.md-button:hover, +.md-button:focus, +.md-button--primary:hover, +.md-button--primary:focus { + background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%); + box-shadow: var(--shadow-accent); + transform: translateY(-2px); +} + +.md-button:active, +.md-button--primary:active { + transform: translateY(0); + box-shadow: var(--shadow-sm); +} + +/* ============================================ + SEARCH - Clean and Functional + ============================================ */ +.md-search__form { + background-color: rgba(255, 255, 255, 0.1); + border-radius: 0.5rem; + transition: all var(--transition-base); +} + +.md-search__form:hover { + background-color: rgba(255, 255, 255, 0.15); +} + +.md-search__input { + background-color: transparent; + color: #f8fafc; + border: none; + padding: 0.5rem 1rem; +} + +.md-search__input::placeholder { + color: #cbd5e1; + opacity: 0.7; +} + +.md-search__input:focus { + outline: 2px solid var(--md-accent-fg-color); + outline-offset: 2px; +} + +/* Search results */ +.md-search-result { + border-radius: 0.5rem; + overflow: hidden; +} + +.md-search-result__item { + border-bottom: 1px solid var(--surface-hover); + transition: background-color var(--transition-fast); +} + +.md-search-result__item:hover { + background-color: var(--surface-hover); +} + +/* ============================================ + CODE BLOCKS - Developer-Friendly with Better Width + ============================================ */ +.md-content code { + background-color: var(--surface-secondary); + color: #e11d48; + padding: 0.15em 0.35em; + border-radius: 0.25rem; + font-size: 0.875em; + font-family: 'Monaco', 'Menlo', 'Consolas', monospace; + border: 1px solid #e2e8f0; +} + +.md-content pre { + background-color: var(--surface-secondary); + border-radius: 0.5rem; + padding: 1.25rem; + overflow-x: auto; + border: 1px solid #e2e8f0; + box-shadow: var(--shadow-sm); + margin: 1.5rem 0; +} + +.md-content pre code { + background-color: transparent; + color: inherit; + padding: 0; + border: none; + font-size: 0.875rem; + line-height: 1.6; +} + +/* Code block copy button */ +.md-clipboard { + opacity: 0.6; + transition: opacity var(--transition-fast); +} + +.md-clipboard:hover { + opacity: 1; +} + +/* ============================================ + TABLES - Organized and Full-Width + ============================================ */ +.md-content table { + border-collapse: separate; + border-spacing: 0; + width: 100%; + border: 1px solid #e2e8f0; + border-radius: 0.5rem; + overflow: hidden; + box-shadow: var(--shadow-sm); + margin: 1.5rem 0; +} + +.md-content th { + background-color: var(--surface-secondary); + color: var(--text-primary); + font-weight: 600; + padding: 0.875rem 1rem; + text-align: left; + border-bottom: 2px solid #e2e8f0; + font-size: 0.875rem; +} + +.md-content td { + padding: 0.875rem 1rem; + border-bottom: 1px solid #f1f5f9; + color: var(--text-secondary); + font-size: 0.9rem; +} + +.md-content tr:last-child td { + border-bottom: none; +} + +.md-content tr:hover { + background-color: var(--surface-hover); +} + +/* ============================================ + ADMONITIONS - Visual Hierarchy + ============================================ */ +.md-content .admonition { + border-left: 4px solid var(--md-accent-fg-color); + border-radius: 0.5rem; + padding: 1rem 1.25rem; + margin: 1.5rem 0; + background-color: var(--surface-secondary); + box-shadow: var(--shadow-sm); +} + +.md-content .admonition-title { + font-weight: 700; + margin-bottom: 0.5rem; + color: var(--text-primary); + font-size: 0.9rem; +} + +/* ============================================ + FOOTER - Full Width (Fixed for all themes) + ============================================ */ +.md-footer { + margin-left: 0; + margin-right: 0; + background-color: #1e293b !important; +} + +.md-footer-meta { + background-color: #0f172a !important; +} + +.md-footer-meta__inner { + color: #cbd5e1 !important; +} + +.md-footer__link, +.md-footer__title, +.md-copyright { + color: #cbd5e1 !important; +} + +.md-footer__link:hover { + color: #f8fafc !important; +} + +.md-social__link { + color: #cbd5e1 !important; +} + +.md-social__link:hover { + color: #f8fafc !important; +} + +/* ============================================ + DARK MODE - Elegant Night Theme + ============================================ */ +[data-md-color-scheme="slate"] { + --text-primary: #f8fafc; + --text-secondary: #cbd5e1; + --text-tertiary: #64748b; + --surface-primary: #0f172a; + --surface-secondary: #1e293b; + --surface-hover: #334155; + --md-accent-fg-color: #60a5fa; + --md-accent-fg-color--hover: #3b82f6; + --md-accent-fg-color--light: rgba(96, 165, 250, 0.1); +} + +[data-md-color-scheme="slate"] .md-header { + background: linear-gradient(135deg, #020617 0%, #0f172a 100%); + box-shadow: 0 1px 0 0 rgba(255, 255, 255, 0.05); +} + +[data-md-color-scheme="slate"] .md-nav__link { + color: var(--text-secondary); +} + +[data-md-color-scheme="slate"] .md-nav__link:hover, +[data-md-color-scheme="slate"] .md-nav__link:focus { + background-color: var(--surface-hover); + color: var(--md-accent-fg-color); +} + +[data-md-color-scheme="slate"] .md-nav__link--active { + color: var(--md-accent-fg-color); + background-color: var(--md-accent-fg-color--light); +} + +[data-md-color-scheme="slate"] .md-content code { + background-color: var(--surface-secondary); + color: #fda4af; + border-color: #334155; +} + +[data-md-color-scheme="slate"] .md-content pre { + background-color: var(--surface-secondary); + border-color: #334155; +} + +[data-md-color-scheme="slate"] .md-content table { + border-color: #334155; +} + +[data-md-color-scheme="slate"] .md-content th { + background-color: var(--surface-secondary); + border-bottom-color: #334155; +} + +[data-md-color-scheme="slate"] .md-content td { + border-bottom-color: #1e293b; +} + +[data-md-color-scheme="slate"] .md-search__form { + background-color: rgba(255, 255, 255, 0.05); +} + +[data-md-color-scheme="slate"] .md-search__form:hover { + background-color: rgba(255, 255, 255, 0.1); +} + +/* ============================================ + SCROLLBAR - Minimal and Polished + ============================================ */ +::-webkit-scrollbar { + width: 10px; + height: 10px; +} + +::-webkit-scrollbar-track { + background: var(--surface-secondary); +} + +::-webkit-scrollbar-thumb { + background: var(--text-tertiary); + border-radius: 5px; +} + +::-webkit-scrollbar-thumb:hover { + background: var(--text-secondary); +} + +/* ============================================ + RESPONSIVE REFINEMENTS + ============================================ */ +@media screen and (max-width: 76.1875em) { + .md-nav__title { + background-color: var(--surface-secondary); + } + + .md-content { + padding: 0 1.5rem !important; + } +} + +@media screen and (max-width: 60em) { + .md-content { + padding: 0 1rem !important; + } + + .md-content h1 { + font-size: 2rem; + } + + .md-content h2 { + font-size: 1.5rem; + } +} + +/* ============================================ + MICRO-INTERACTIONS - Delightful Details + ============================================ */ +.md-content a, +.md-nav__link, +.md-button { + will-change: transform; +} + +.md-tabs__link { + opacity: 0.85; + transition: opacity var(--transition-fast); +} + +.md-tabs__link:hover { + opacity: 1; +} + +/* Focus visible for accessibility */ +*:focus-visible { + outline: 2px solid var(--md-accent-fg-color); + outline-offset: 2px; + border-radius: 0.25rem; +} + +/* ============================================ + PRINT STYLES - Optimized for Documentation + ============================================ */ +@media print { + .md-sidebar, + .md-header, + .md-footer { + display: none; + } + + .md-content { + margin: 0 !important; + max-width: none !important; + } +} + +/* ============================================ + MATHJAX - Responsive Font Scaling + ============================================ */ +.MathJax { + font-size: calc(125% + (150% - 125%) * (100vw - 320px) / (1920px - 320px)) !important; +} diff --git a/docs/generation/markdown/decorators.md b/docs/generation/markdown/decorators.md deleted file mode 100644 index 2dcff31..0000000 --- a/docs/generation/markdown/decorators.md +++ /dev/null @@ -1,396 +0,0 @@ -# Decorators - Making Code Reactive - -Decorators in FynX provide the high-level API for creating reactive relationships in your application. They transform regular Python functions and classes into reactive components that automatically respond to state changes. - -## Overview - -FynX decorators fall into two main categories: - -- **Function Decorators**: Transform functions into reactive components (`@reactive`) -- **Class Decorators**: Make class attributes reactive (`@observable`) - -## Function Decorators - -### `@reactive` - Automatic Function Execution - -The `@reactive` decorator makes functions automatically execute whenever their observable dependencies change. It's perfect for side effects like UI updates, logging, and API calls. - -#### Basic Usage - -```python -from fynx import reactive, observable - -counter = observable(0) - -@reactive(counter) -def log_counter_changes(new_value): - print(f"Counter changed to: {new_value}") - -counter = 5 # Prints: "Counter changed to: 5" -counter = 10 # Prints: "Counter changed to: 10" -``` - -#### Multiple Dependencies - -```python -user_name = observable("Alice") -user_age = observable(30) - -@reactive(user_name, user_age) -def update_user_display(name, age): - print(f"User: {name}, Age: {age}") - -user_name = "Bob" # Triggers with current age -user_age = 31 # Triggers with current name -``` - -#### Store-Level Reactions - -React to any change in an entire store: - -```python -from fynx import Store, observable, reactive - -class UserStore(Store): - name = observable("Alice") - age = observable(30) - email = observable("alice@example.com") - -@reactive(UserStore) -def on_any_user_change(store_snapshot): - print(f"User data changed: {store_snapshot.name}, {store_snapshot.age}") - -UserStore.name = "Bob" # Triggers reaction -UserStore.email = "bob@example.com" # Also triggers reaction -``` - -#### Cleanup - -Reactive functions can be unsubscribed: - -```python -# The decorator returns the original function -# so you can unsubscribe later -unsubscribe_func = reactive(UserStore)(on_any_user_change) - -# Later, stop the reaction -UserStore.unsubscribe(on_any_user_change) -``` - -### Conditional Reactions with @reactive - -You can use `@reactive` with conditional observables to create event-driven reactions: - -```python -from fynx import reactive, observable - -count = observable(5) - -# Create a conditional observable -is_above_threshold = count >> (lambda c: c > 10) - -@reactive(is_above_threshold) -def on_threshold(is_above): - if is_above: - print("Count exceeded threshold!") - -count.set(15) # Triggers the reaction -count.set(8) # No reaction (condition not met) -count.set(12) # Triggers the reaction again -``` - -## Class Decorators - -### `@observable` - Reactive Class Attributes - -The `@observable` decorator makes class attributes reactive. It's used within Store classes to create observable properties. - -#### Basic Usage in Stores - -```python -from fynx import Store, observable - -class CounterStore(Store): - count = observable(0) # Reactive attribute - step = observable(1) # Another reactive attribute - -# Direct assignment triggers reactivity -CounterStore.count = 5 -CounterStore.step = 2 -``` - -#### Computed Properties - -While not a decorator itself, the `>>` operator (used with `observable` attributes) creates computed properties: - -```python -class CounterStore(Store): - count = observable(0) - doubled = count >> (lambda x: x * 2) # Computed property - -CounterStore.count = 5 -print(CounterStore.doubled) # 10 -``` - -## Decorator Patterns - -### When to Use Each Decorator - -| Decorator | Use Case | Triggers On | -|-----------|----------|-------------| -| `@reactive` | Side effects, UI updates, logging, conditional reactions | Every change to dependencies | -| `@observable` | Reactive state in classes | N/A (attribute decorator) | - -### Combining Decorators - -```python -from fynx import Store, observable, reactive - -class TodoStore(Store): - todos = observable([]) - filter_mode = observable("all") - - # Reactive: Update UI on any change - @reactive(todos, filter_mode) - def update_ui(todos_list, mode): - print(f"UI updated: {len(todos_list)} todos, filter: {mode}") - - # Conditional reactive: Only when todos exist and filter changes to "completed" - completed_filter = (todos >> (lambda t: len(t) > 0)) & (filter_mode >> (lambda f: f == "completed")) - @reactive(completed_filter) - def show_completion_message(should_show): - if should_show: - completed_count = len([t for t in TodoStore.todos if t["completed"]]) - print(f"🎉 {completed_count} todos completed!") -``` - -### Error Handling - -Decorators handle errors gracefully: - -```python -@reactive(some_observable) -def potentially_failing_reaction(value): - if value < 0: - raise ValueError("Negative values not allowed!") - print(f"Processed: {value}") - -# Errors in reactive functions don't break the reactive system -some_observable = -5 # Error logged, but reactivity continues -some_observable = 10 # Continues working: "Processed: 10" -``` - -### Performance Considerations - -#### Lazy vs Eager Execution - -- **Reactive methods** (`@reactive`): Execute eagerly when dependencies change -- **Computed properties**: Execute lazily when accessed - -```python -expensive_calc = observable(0) - -# Eager: Runs immediately when expensive_calc changes -@reactive(expensive_calc) -def eager_update(val): - slow_operation(val) # Runs immediately - -# Lazy: Only runs when result is accessed -lazy_result = expensive_calc >> (lambda val: slow_operation(val)) - -# lazy_result.value # Only runs slow_operation here -``` - -#### Memory Management - -Always clean up subscriptions: - -```python -class Component: - def __init__(self): - # Store subscription references for cleanup - self._cleanup = reactive(store)(self._on_change) - - def destroy(self): - # Clean up when component is destroyed - store.unsubscribe(self._on_change) -``` - -## Common Patterns - -### UI State Management - -```python -class UIStore(Store): - sidebar_open = observable(False) - modal_visible = observable(False) - loading = observable(False) - - @reactive(sidebar_open) - def update_layout(open_state): - if open_state: - print("📱 Adjusting layout for sidebar") - else: - print("📱 Restoring full-width layout") - - # Conditional reactive for loading state - @reactive(loading) - def handle_loading_state(is_loading): - if is_loading: - print("⏳ Showing loading spinner") - else: - print("✅ Hiding loading spinner") -``` - -### Form Handling - -```python -class FormStore(Store): - email = observable("") - password = observable("") - confirm_password = observable("") - - # Reactive validation - @reactive(email) - def validate_email(email_val): - if email_val and "@" not in email_val: - print("❌ Invalid email format") - - # Conditional reactive for complete form - form_valid = ( - email >> (lambda e: e and "@" in e) & - password >> (lambda p: len(p) >= 8) & - (password | confirm_password) >> (lambda p, c: p == c) - ) - @reactive(form_valid) - def enable_submit(is_valid): - if is_valid: - print("✅ Form is valid and ready to submit") -``` - -### API Integration - -```python -class ApiStore(Store): - is_loading = observable(False) - data = observable(None) - error = observable(None) - - @reactive(is_loading) - def update_ui_state(loading): - if loading: - print("⏳ Showing loading indicator") - else: - print("✅ Hiding loading indicator") - - # Conditional reactive for error state - @reactive(error) - def handle_error_state(error_val): - if error_val is not None: - print(f"❌ Error: {error_val}") - - # Conditional reactive for successful response - @reactive(data) - def process_successful_response(data_val): - if data_val is not None: - print(f"📦 Processing data: {len(data_val)} items") -``` - -## Best Practices - -### 1. Use Descriptive Function Names - -```python -# Good -@reactive(user_data) -def update_user_profile_display(user): - pass - -# Avoid -@reactive(user_data) -def func1(user): - pass -``` - -### 2. Keep Reactive Functions Focused - -```python -# Good: Single responsibility -@reactive(shopping_cart) -def update_cart_total(cart): - calculate_total(cart) - -@reactive(shopping_cart) -def update_cart_item_count(cart): - update_counter(len(cart)) - -# Avoid: Multiple responsibilities -@reactive(shopping_cart) -def handle_cart_changes(cart): - calculate_total(cart) - update_counter(len(cart)) - send_analytics(cart) - update_ui(cart) -``` - -### 3. Handle Errors Appropriately - -```python -@reactive(api_response) -def handle_api_response(response): - try: - if response["error"]: - show_error(response["error"]) - else: - process_data(response["data"]) - except Exception as e: - log_error(f"Failed to handle API response: {e}") - show_generic_error() -``` - -### 4. Use Conditional Observables for State Machines - -```python -current_state = observable("idle") - -# Create conditional observables for state transitions -is_loading = current_state >> (lambda s: s == "loading") -is_success = current_state >> (lambda s: s == "success") -is_error = current_state >> (lambda s: s == "error") - -@reactive(is_loading) -def enter_loading_state(is_loading_state): - if is_loading_state: - show_spinner() - -@reactive(is_success) -def enter_success_state(is_success_state): - if is_success_state: - hide_spinner() - show_success_message() - -@reactive(is_error) -def enter_error_state(is_error_state): - if is_error_state: - hide_spinner() - show_error_message() -``` - -### 5. Document Side Effects - -```python -@reactive(user_preferences) -def update_application_theme(preferences): - """ - Updates the application theme based on user preferences. - - Side effects: - - Modifies CSS custom properties - - Updates localStorage - - Triggers re-render of styled components - """ - apply_theme(preferences["theme"]) - save_to_localstorage("theme", preferences["theme"]) -``` - -Decorators are the bridge between reactive state and imperative code. They allow you to write declarative relationships while maintaining clean, maintainable code. Use `@reactive` for all reactive behavior, combining it with conditional observables for event-driven reactions. Remember that reactive functions should focus on side effects while computed properties handle pure transformations. diff --git a/docs/index.md b/docs/generation/markdown/index.md similarity index 87% rename from docs/index.md rename to docs/generation/markdown/index.md index 2979b1a..c24edff 100644 --- a/docs/index.md +++ b/docs/generation/markdown/index.md @@ -1,15 +1,13 @@ -
Hey! You got here early! FynX is still incredibly new, so we're still ironing out the documentation here. Thanks for your patience and early interest!
+Hey! You got here early! FynX is still incredibly new, so we're still ironing out the documentation here. Thanks for your patience and early interest!
-
+
@@ -38,55 +36,39 @@
FynX is a lightweight reactive state management library for Python that brings the elegance of reactive programming to your applications. Inspired by MobX, FynX eliminates the complexity of manual state synchronization by automatically propagating changes through your application's data flow. When one piece of state changes, everything that depends on it updates automatically—no boilerplate, no explicit update calls, just transparent reactivity.
+## Installation
-## Understanding Reactive Programming
-
-Traditional imperative programming requires you to manually orchestrate updates: when data changes, you must explicitly call update methods, refresh UI components, or invalidate caches. This creates brittle, error-prone code where it's easy to forget an update or create inconsistent states.
-
-Reactive programming inverts this model. Instead of imperatively triggering updates, you declare relationships between data. When a value changes, the framework automatically propagates that change to everything that depends on it. Think of it like a spreadsheet: when you change a cell, all formulas referencing that cell recalculate automatically. FynX brings this same automatic dependency tracking and update propagation to Python.
-
-What makes FynX special is its transparency. You don't need to learn special syntax or wrap everything in framework-specific abstractions. Just use normal Python objects and assignment—FynX handles the reactivity behind the scenes through automatic dependency tracking.
-
-
-## Core Concepts
-
-FynX's design centers on four fundamental building blocks that work together to create reactive data flows:
-
-### Observables
-
-Observables are the foundation of reactivity. An observable is simply a value that FynX watches for changes. When you modify an observable, FynX automatically notifies everything that depends on it. Think of observables as the source nodes in your application's dependency graph—they're the raw state that drives everything else.
+Install FynX from PyPI using pip:
-### Computed Values
+```bash
+pip install fynx
+```
-Computed values are derived data that automatically recalculates when their dependencies change. They provide memoization by default, meaning they only recompute when one of their inputs actually changes—not on every access. This makes them both convenient and performant for expensive calculations. Computed values form the intermediate nodes in your dependency graph, transforming observables into the exact shape your application needs.
+FynX has no required dependencies and works with Python 3.9 and above.
-### Reactions
-Reactions are side effects that execute automatically when their observed dependencies change. Use reactions for actions like updating a UI, making an API call, logging, or any other effect that should happen in response to state changes. While observables and computed values represent your data, reactions represent what your application does with that data.
+## Why Use FynX?
-### Stores
+**Transparent Reactivity**: FynX requires no special syntax. Use standard Python assignment, method calls, and attribute access—reactivity works automatically without wrapper objects or proxy patterns.
-Stores provide organizational structure by grouping related observables, computed values, and methods together. They offer convenient patterns for subscribing to changes and managing related state as a cohesive unit. Stores aren't required, but they help you organize complex state into logical, reusable components.
+**Automatic Dependency Tracking**: FynX observables track their dependents automatically during execution. You never manually register or unregister dependencies; the framework infers them from how your code actually runs.
-### Conditional Reactions
+**Lazy Evaluation with Memoization**: Computed values only recalculate when their dependencies change, and only when accessed. This combines the convenience of automatic updates with the performance of intelligent caching.
-Conditional reactions extend the basic reaction pattern by only executing when specific conditions are met. They're perfect for implementing state machines, validation rules, or any scenario where you need fine-grained control over when effects trigger. This allows you to express complex conditional logic declaratively rather than scattering imperative checks throughout your code.
+**Composable Architecture**: Observables, computed values, and reactions compose naturally. You can nest stores, chain computed values, and combine reactions to build complex reactive systems from simple, reusable pieces.
-## Why FynX?
+**Expressive Operators**: FynX provides intuitive operators (`+`, `>>`, `&`, `~`, `|`) that let you compose reactive logic clearly and concisely, making your data flow explicit and easy to understand.
-**Transparent Reactivity**: FynX requires no special syntax. Use standard Python assignment, method calls, and attribute access—reactivity works automatically without wrapper objects or proxy patterns.
-**Automatic Dependency Tracking**: FynX observables track their dependents automatically during execution. You never manually register or unregister dependencies; the framework infers them from how your code actually runs.
+## Understanding Reactive Programming
-**Lazy Evaluation with Memoization**: Computed values only recalculate when their dependencies change, and only when accessed. This combines the convenience of automatic updates with the performance of intelligent caching.
+Traditional imperative programming requires you to manually orchestrate updates: when data changes, you must explicitly call update methods, refresh UI components, or invalidate caches. This creates brittle, error-prone code where it's easy to forget an update or create inconsistent states.
-**Full Type Safety**: FynX provides complete type hints, giving you autocomplete, inline documentation, and static analysis throughout your reactive code.
+Reactive programming inverts this model. Instead of imperatively triggering updates, you declare relationships between data. When a value changes, the framework automatically propagates that change to everything that depends on it. Think of it like a spreadsheet: when you change a cell, all formulas referencing that cell recalculate automatically. FynX brings this same automatic dependency tracking and update propagation to Python.
-**Memory Efficient**: FynX automatically cleans up reactive contexts when they're no longer needed, preventing memory leaks in long-running applications.
+What makes FynX special is its transparency. You don't need to learn special syntax or wrap everything in framework-specific abstractions. Just use normal Python objects and assignment—FynX handles the reactivity behind the scenes through automatic dependency tracking.
-**Composable Architecture**: Observables, computed values, and reactions compose naturally. You can nest stores, chain computed values, and combine reactions to build complex reactive systems from simple, reusable pieces.
-**Expressive Operators**: FynX provides intuitive operators (`|`, `>>`, `&`, `~`) that let you compose reactive logic clearly and concisely, making your data flow explicit and easy to understand.
## Quick Start Example
@@ -102,7 +84,7 @@ class UserStore(Store):
is_online = observable(False)
# Computed property that automatically updates when dependencies change
- greeting = (name | age) >> (
+ greeting = (name + age) >> (
lambda n, a: f"Hello, {n}! You are {a} years old."
)
@@ -126,15 +108,31 @@ UserStore.is_online = True # Prints: Adult user Bob is now online!
Notice how natural the code looks—no explicit update calls, no subscription management, just straightforward Python that happens to be reactive.
-## Installation
-Install FynX from PyPI using pip:
+## Core Concepts
-```bash
-pip install fynx
-```
+FynX's design centers on four fundamental building blocks that work together to create reactive data flows:
+
+### Observables
+
+Observables are the foundation of reactivity. An observable is simply a value that FynX watches for changes. When you modify an observable, FynX automatically notifies everything that depends on it. Think of observables as the source nodes in your application's dependency graph—they're the raw state that drives everything else.
+
+### Computed Values
+
+Computed values are derived data that automatically recalculates when their dependencies change. They provide memoization by default, meaning they only recompute when one of their inputs actually changes—not on every access. This makes them both convenient and performant for expensive calculations. Computed values form the intermediate nodes in your dependency graph, transforming observables into the exact shape your application needs.
+
+### Reactions
+
+Reactions are side effects that execute automatically when their observed dependencies change. Use reactions for actions like updating a UI, making an API call, logging, or any other effect that should happen in response to state changes. While observables and computed values represent your data, reactions represent what your application does with that data.
+
+### Stores
+
+Stores provide organizational structure by grouping related observables, computed values, and methods together. They offer convenient patterns for subscribing to changes and managing related state as a cohesive unit. Stores aren't required, but they help you organize complex state into logical, reusable components.
+
+### Conditional Reactions
+
+Conditional reactions extend the basic reaction pattern by only executing when specific conditions are met. They're perfect for implementing state machines, validation rules, or any scenario where you need fine-grained control over when effects trigger. This allows you to express complex conditional logic declaratively rather than scattering imperative checks throughout your code.
-FynX has no required dependencies and works with Python 3.8 and above.
## Common Patterns
@@ -148,7 +146,7 @@ As you work with FynX, you'll find these patterns emerge naturally:
**Conditional Logic**: Use watch decorators to implement state machines, validation rules, or event filtering. This keeps conditional logic declarative and colocated with the relevant state.
-**Data Flow Composition**: Use FynX's operators (`|` for piping values, `>>` for chaining, `&` for combining) to build clear, expressive data transformation pipelines.
+**Data Flow Composition**: Use FynX's operators (`+` for piping values, `>>` for chaining, `&` for combining) to build clear, expressive data transformation pipelines.
## Documentation
diff --git a/docs/generation/markdown/mathematical-foundations.md b/docs/generation/markdown/mathematical/mathematical-foundations.md
similarity index 95%
rename from docs/generation/markdown/mathematical-foundations.md
rename to docs/generation/markdown/mathematical/mathematical-foundations.md
index b9cd7f8..2e874fa 100644
--- a/docs/generation/markdown/mathematical-foundations.md
+++ b/docs/generation/markdown/mathematical/mathematical-foundations.md
@@ -101,10 +101,10 @@ You want to combine two separate time-varying values into a single time-varying
first_name: Observable[str] = observable("Jane")
last_name: Observable[str] = observable("Doe")
-full_name_data: Observable[tuple[str, str]] = first_name | last_name
+full_name_data: Observable[tuple[str, str]] = first_name + last_name
```
-The `|` operator creates this combination. But what exactly has happened here? Category theory gives us a precise answer: we've constructed a product.
+The `+` operator creates this combination. But what exactly has happened here? Category theory gives us a precise answer: we've constructed a product.
The product satisfies an elegant isomorphism:
@@ -121,22 +121,22 @@ Formally, if you have any way of using two observables together, that usage fact
In practice, this manifests in a useful way. Consider:
```python
-product = first_name | last_name
+product = first_name + last_name
full_name = product >> (lambda t: f"{t[0]} {t[1]}")
initials = product >> (lambda t: f"{t[0][0]}.{t[1][0]}.")
display = product >> (lambda t: f"{t[1]}, {t[0]}")
```
-All three computations need both names. The universal property proves they're all talking about the *same* product. FynX computes `first_name | last_name` once, not three times. The optimizer can safely share this computation because the mathematics guarantees all three uses refer to the same mathematical object.
+All three computations need both names. The universal property proves they're all talking about the *same* product. FynX computes `first_name + last_name` once, not three times. The optimizer can safely share this computation because the mathematics guarantees all three uses refer to the same mathematical object.
### Symmetry and Associativity
Products have nice algebraic properties. They're symmetric—order doesn't affect the structure:
```python
-ab = first_name | last_name # Observable[tuple[str, str]]
-ba = last_name | first_name # Observable[tuple[str, str]]
+ab = first_name + last_name # Observable[tuple[str, str]]
+ba = last_name + first_name # Observable[tuple[str, str]]
```
These are isomorphic. Same information, different tuple order. The structure is preserved.
@@ -146,8 +146,8 @@ They're also associative—grouping doesn't matter:
```python
city = observable("New York")
-left = (first_name | last_name) | city
-right = first_name | (last_name | city)
+left = (first_name + last_name) + city
+right = first_name + (last_name + city)
```
The nesting differs, but structurally, all three observables are combined correctly. Changes to any propagate through as expected.
@@ -193,7 +193,7 @@ Passenger Data
↓
[Passport Control] ← is_in_range check
↓
-[Security Screening] ← is_stable check
+[Security Screening] ← is_stable check
↓
[Boarding Pass Validation] ← has_signal check
↓
@@ -209,7 +209,7 @@ When you write `data & condition1 & condition2`, you're building this chain of c
```python
sensor_reading.set(42.5) # All conditions pass → gate opens
sensor_reading.set(150) # Fails range check → gate closes
-sensor_reading.set(-10) # Fails range check → gate stays closed
+sensor_reading.set(-10) # Fails range check → gate stays closed
sensor_reading.set(55.0) # All conditions pass → gate opens again
```
@@ -300,8 +300,8 @@ The pullback is a new object (often written $X \times_Z Y$) along with projectio
```
X ×_Z Y ----→ Y
- | |
- | | g
+ + +
+ + + g
↓ ↓
X ---f--→ Z
```
@@ -318,8 +318,8 @@ We're interested in values where all conditions map to `True`. Visually, for a s
```
ConditionalObservable ----→ {True}
- | |
- | π |
+ + +
+ + π +
↓ ↓
Source Observable ---c--→ Observable[Bool]
```
@@ -328,7 +328,7 @@ This is a pullback along the morphism selecting `True` from the boolean domain.
For multiple conditions, we're taking the intersection of multiple such fibers:
-$\text{ConditionalObservable}(s, c_1, \ldots, c_n) \cong \{ x \in s \mid c_1(x) \wedge c_2(x) \wedge \cdots \wedge c_n(x) \}$
+$$\text{ConditionalObservable}(s, c_1, \ldots, c_n) \cong \{ x \in s \mid c_1(x) \wedge c_2(x) \wedge \cdots \wedge c_n(x) \}$$
Each condition creates a checkpoint. The conditional observable represents values that clear all checkpoints—the pullback ensures this subset is well-defined categorically.
@@ -394,7 +394,7 @@ is_valid_email = email >> (lambda e: "@" in e and "." in e)
is_strong_password = password >> (lambda p: len(p) >= 8)
# Product: combine related fields
-passwords_match = (password | confirmation) >> (lambda pc: pc[0] == pc[1])
+passwords_match = (password + confirmation) >> (lambda pc: pc[0] == pc[1])
# Pullback: form is valid only when all conditions hold
form_valid = email & is_valid_email & is_strong_password & passwords_match & terms_accepted
@@ -430,7 +430,7 @@ The optimizer applies four types of rewrites, each justified by category theory.
The composition law proves that sequential transformations can safely fuse:
-$$\text{obs} \gg f \gg g \gg h \rightsquigarrow \text{obs} \gg (h \circ g \circ f)$$
+$$\text{obs} \gg f \gg g \gg h \rightarrow \text{obs} \gg (h \circ g \circ f)$$
Instead of creating intermediate observables for each `>>`, FynX fuses the entire chain into a single computed observable. This is why deep chains stay efficient—they're not actually thousands of separate observables, just composed functions in one observable.
@@ -442,12 +442,12 @@ The universal property of products proves that multiple computations needing the
```python
# User writes:
-result1 = (a | b) >> f
-result2 = (a | b) >> g
-result3 = (a | b) >> h
+result1 = (a + b) >> f
+result2 = (a + b) >> g
+result3 = (a + b) >> h
# Optimizer produces:
-product = a | b
+product = a + b
result1 = product >> f
result2 = product >> g
result3 = product >> h
@@ -459,7 +459,7 @@ The product is computed once. When 47,000 components depend on a single product,
The commutativity and associativity of Boolean pullbacks allow combining sequential filters:
-$$\text{obs} \& c_1 \& c_2 \& c_3 \rightsquigarrow \text{obs} \& (c_1 \wedge c_2 \wedge c_3)$$
+$$\text{obs} \& c_1 \& c_2 \& c_3 \rightarrow \text{obs} \& (c_1 \wedge c_2 \wedge c_3)$$
Multiple conditional checks become one. The algebraic structure proves this fusion preserves semantics.
@@ -467,7 +467,7 @@ Multiple conditional checks become one. The algebraic structure proves this fusi
The optimizer decides whether to cache or recompute each node using a cost model:
-$C(\sigma) = \alpha \cdot |\text{Dep}(\sigma)| + \beta \cdot \mathbb{E}[\text{Updates}(\sigma)] + \gamma \cdot \text{depth}(\sigma)$
+$$C(\sigma) = \alpha \cdot +\text{Dep}(\sigma)+ + \beta \cdot \mathbb{E}[\text{Updates}(\sigma)] + \gamma \cdot \text{depth}(\sigma)$$
This cost functional has important mathematical structure. It's a monoidal functor from the reactive category to the ordered monoid $(\mathbb{R}^+, +, 0)$. This means:
@@ -557,7 +557,7 @@ This batching provides two benefits: each observable updates once per batch rath
FynX's benchmark suite measures fundamental operations:
- Observable creation: 794,000 ops/sec
-- Individual updates: 353,000 ops/sec
+- Individual updates: 353,000 ops/sec
- Chain propagation: 1,640 ops/sec for 2,776-link chains
- Reactive fan-out: 47,000 ops/sec with 47,427 dependent components
@@ -593,6 +593,6 @@ We've explored how category theory provides foundations for reactive programming
The mathematical foundations serve three purposes: they prove compositions work correctly, they show where optimizations preserve semantics, and they enable natural composition of observables.
-Understanding these foundations isn't required to use FynX effectively. The `>>`, `|`, and `&` operators work intuitively without knowing category theory. But the mathematics explains why the library behaves as it does, why certain design decisions were made, and why optimizations are valid.
+Understanding these foundations isn't required to use FynX effectively. The `>>`, `+`, and `&` operators work intuitively without knowing category theory. But the mathematics explains why the library behaves as it does, why certain design decisions were made, and why optimizations are valid.
-Category theory transforms reactive programming from a collection of patterns into a structured system with provable properties. The elegance is that complexity becomes composability. Theory becomes tool. And mathematics guides engineering toward correctness.
\ No newline at end of file
+Category theory transforms reactive programming from a collection of patterns into a structured system with provable properties. The elegance is that complexity becomes composability. Theory becomes tool. And mathematics guides engineering toward correctness.
diff --git a/docs/generation/markdown/reactive-decorator.md b/docs/generation/markdown/reactive-decorator.md
deleted file mode 100644
index 4e3dc51..0000000
--- a/docs/generation/markdown/reactive-decorator.md
+++ /dev/null
@@ -1,5 +0,0 @@
-# @reactive Decorator
-
-Decorator for creating reactive functions that run when observables change.
-
-::: fynx.reactive
diff --git a/docs/generation/markdown/api.md b/docs/generation/markdown/reference/api.md
similarity index 88%
rename from docs/generation/markdown/api.md
rename to docs/generation/markdown/reference/api.md
index bcb8fb4..f4b96eb 100644
--- a/docs/generation/markdown/api.md
+++ b/docs/generation/markdown/reference/api.md
@@ -1,7 +1,5 @@
# API Reference
-
-
This reference provides comprehensive documentation of FynX's public API. FynX is a reactive programming library that makes your application state respond automatically to changes—think of it as a spreadsheet for your code, where updating one cell automatically recalculates all the formulas that depend on it.
## A Mental Model for FynX
@@ -12,7 +10,7 @@ Before diving into the API details, it helps to understand FynX's core philosoph
**FynX is declarative**: You describe *relationships* between values, and FynX handles the updates automatically. Change a value once, and everything that depends on it updates correctly, in the right order, every time.
-This mental shift—from managing updates to declaring relationships—is the key to thinking reactively.
+This mental shift—from managing updates to declaring relationships—is what makes thinking reactively so powerful.
## Your Learning Path
@@ -33,15 +31,15 @@ Observables are containers for values that change over time. Unlike regular vari
**[Observable](observable.md)** — The foundation of FynX. Create observables with `observable(initial_value)`, read them with `.value`, write them with `.set(new_value)`. Every other FynX feature builds on this simple primitive.
-**[ComputedObservable](computed-observable.md)** — Values that automatically recalculate when their dependencies change. Create them with the `>>` operator: `full_name = (first | last) >> (lambda f, l: f"{f} {l}")`. The `>>` operator transforms observables through functions, creating a new computed observable. Alternatively, use the `.then(func)` method on observables for the same result. FynX tracks dependencies automatically and ensures computed values always stay up-to-date.
+**[ComputedObservable](computed-observable.md)** — Values that automatically recalculate when their dependencies change. Create them with the `>>` operator: `full_name = (first + last) >> (lambda f, l: f"{f} {l}")`. The `>>` operator transforms observables through functions, creating a new computed observable. Alternatively, use the `.then(func)` method on observables for the same result. FynX tracks dependencies automatically and ensures computed values always stay up-to-date.
-**[MergedObservable](merged-observable.md)** — Combine multiple observables into a single reactive tuple using the `|` operator: `position = x | y | z`. When any source changes, subscribers receive all values as a tuple. This is the foundation for reactive relationships that depend on multiple values.
+**[MergedObservable](merged-observable.md)** — Combine multiple observables into a single reactive tuple using the `+` operator: `position = x + y + z`. When any source changes, subscribers receive all values as a tuple. This is the foundation for reactive relationships that depend on multiple values.
**[ConditionalObservable](conditional-observable.md)** — Observables that emit when conditions are satisfied. Create them with the `&` operator: `valid_submission = form_data & is_valid`. This enables sophisticated reactive logic without cluttering your code with conditional checks.
**[Observable Descriptors](observable-descriptors.md)** — The mechanism behind Store class attributes. When you write `name = observable("Alice")` in a Store class, you're creating a descriptor that provides clean property access without `.value` or `.set()`.
-**[Observable Operators](observable-operators.md)** — The operators (`|`, `>>`, `&`, `~`) and methods (`.then()`, `.also()`) that let you compose observables into reactive pipelines. The `>>` operator is the primary way to transform observables, passing values through functions. Understanding these operators unlocks FynX's full expressive power.
+**[Observable Operators](observable-operators.md)** — The operators (`+`, `>>`, `&`, `~`) and methods (`.then()`, `.requiring()`) that let you compose observables into reactive pipelines. The `>>` operator is the primary way to transform observables, passing values through functions. Understanding these operators unlocks FynX's full expressive power.
### Stores: Organizing State
@@ -89,11 +87,11 @@ AppStore.count = current + 1 # Write
```python
# Using the >> operator (recommended)
doubled = count >> (lambda c: c * 2)
-full_name = (first | last) >> (lambda f, l: f"{f} {l}")
+full_name = (first + last) >> (lambda f, l: f"{f} {l}")
# Using .then() method (alternative syntax)
doubled = count.then(lambda c: c * 2)
-full_name = (first | last).then(lambda f, l: f"{f} {l}")
+full_name = (first + last).then(lambda f, l: f"{f} {l}")
```
### Reacting to Changes
@@ -119,7 +117,7 @@ def on_threshold(is_above):
```python
# Merge multiple sources
-position = x | y | z
+position = x + y + z
# Transform values with >> operator
doubled = count >> (lambda c: c * 2)
@@ -151,12 +149,12 @@ subtotal = ShoppingCartStore.items >> (
lambda items: sum(item['price'] * item['quantity'] for item in items)
)
-discount_amount = (ShoppingCartStore.items | ShoppingCartStore.discount_code) >> (
+discount_amount = (ShoppingCartStore.items + ShoppingCartStore.discount_code) >> (
lambda items, code: sum(item['price'] * item['quantity'] for item in items) * 0.20
if code == "SAVE20" else 0.0
)
-total = (subtotal | discount_amount) >> (
+total = (subtotal + discount_amount) >> (
lambda sub, disc: sub - disc
)
@@ -191,29 +189,34 @@ ShoppingCartStore.discount_code = "SAVE20"
Throughout this reference, we follow consistent patterns:
-- **Type signatures** use Python type hints for clarity and enable IDE autocomplete
-- **Examples progress from simple to complex** within each page
-- **Notes highlight gotchas** that trip up newcomers
-- **Performance tips** appear when relevant to optimization decisions
-- **See also links** connect related concepts and alternative approaches
+* **Type signatures** use Python type hints for clarity and enable IDE autocomplete
+* **Examples progress from simple to complex** within each page
+* **Notes highlight gotchas** that trip up newcomers
+* **Performance tips** appear when relevant to optimization decisions
+* **See also links** connect related concepts and alternative approaches
## Navigating This Reference
### New to FynX?
+
Read in order: [Observable](observable.md) → [Store](store.md) → [@reactive](reactive-decorator.md) → [ConditionalObservable](conditional-observable.md)
### Building an application?
+
Focus on: [Store](store.md), [Observable Operators](observable-operators.md) (especially `>>`), [@reactive](reactive-decorator.md)
### Need complex state logic?
+
Dive into: [Observable Operators](observable-operators.md), [ConditionalObservable](conditional-observable.md), [@reactive](reactive-decorator.md)
### Performance optimization?
+
See: [ComputedObservable](computed-observable.md) for memoization, [Observable](observable.md) for subscription management
### Curious about implementation?
+
Explore: [Observable Descriptors](observable-descriptors.md) to understand how the magic works
----
+***
For conceptual introductions and tutorials, return to the [main documentation](../../index.md).
diff --git a/docs/generation/markdown/computed-observable.md b/docs/generation/markdown/reference/computed-observable.md
similarity index 83%
rename from docs/generation/markdown/computed-observable.md
rename to docs/generation/markdown/reference/computed-observable.md
index 2d0815b..f820059 100644
--- a/docs/generation/markdown/computed-observable.md
+++ b/docs/generation/markdown/reference/computed-observable.md
@@ -58,7 +58,7 @@ result = count >> double >> add_one
## Key Properties
-- **Reactive**: Automatically update when source observables change
-- **Immutable**: Don't modify source values, create new derived values
-- **Composable**: Can be chained and combined with other observables
-- **Lazy**: Only compute when subscribed to or when source changes
+* **Reactive**: Automatically update when source observables change
+* **Immutable**: Don't modify source values, create new derived values
+* **Composable**: Can be chained and combined with other observables
+* **Lazy**: Only compute when subscribed to or when source changes
diff --git a/docs/generation/markdown/conditional-observable.md b/docs/generation/markdown/reference/conditional-observable.md
similarity index 82%
rename from docs/generation/markdown/conditional-observable.md
rename to docs/generation/markdown/reference/conditional-observable.md
index 88ef758..1f229d8 100644
--- a/docs/generation/markdown/conditional-observable.md
+++ b/docs/generation/markdown/reference/conditional-observable.md
@@ -60,7 +60,7 @@ count.set(4) # Prints: Positive even: 4
## Key Properties
-- **Filtering**: Only emit values that satisfy all conditions
-- **Reactive**: Automatically re-evaluate conditions when source changes
-- **Composable**: Can be combined with other observables using `&`, `|`, and `>>`
-- **Efficient**: Conditions are only evaluated when source values change
+* **Filtering**: Only emit values that satisfy all conditions
+* **Reactive**: Automatically re-evaluate conditions when source changes
+* **Composable**: Can be combined with other observables using `&`, `+`, and `>>`
+* **Efficient**: Conditions are only evaluated when source values change
diff --git a/docs/generation/markdown/merged-observable.md b/docs/generation/markdown/reference/merged-observable.md
similarity index 92%
rename from docs/generation/markdown/merged-observable.md
rename to docs/generation/markdown/reference/merged-observable.md
index 379f30e..f86a2c0 100644
--- a/docs/generation/markdown/merged-observable.md
+++ b/docs/generation/markdown/reference/merged-observable.md
@@ -1,5 +1,5 @@
# MergedObservable
-Observables that combine multiple values using the merge operator (`|`).
+Observables that combine multiple values using the merge operator (`+`).
::: fynx.observable.merged
diff --git a/docs/generation/markdown/observable-descriptors.md b/docs/generation/markdown/reference/observable-descriptors.md
similarity index 100%
rename from docs/generation/markdown/observable-descriptors.md
rename to docs/generation/markdown/reference/observable-descriptors.md
diff --git a/docs/generation/markdown/observable-operators.md b/docs/generation/markdown/reference/observable-operators.md
similarity index 100%
rename from docs/generation/markdown/observable-operators.md
rename to docs/generation/markdown/reference/observable-operators.md
diff --git a/docs/generation/markdown/observable.md b/docs/generation/markdown/reference/observable.md
similarity index 100%
rename from docs/generation/markdown/observable.md
rename to docs/generation/markdown/reference/observable.md
diff --git a/docs/generation/markdown/reference/reactive-decorator.md b/docs/generation/markdown/reference/reactive-decorator.md
new file mode 100644
index 0000000..075cb8d
--- /dev/null
+++ b/docs/generation/markdown/reference/reactive-decorator.md
@@ -0,0 +1,299 @@
+# Understanding the @reactive Decorator
+
+The `@reactive` decorator bridges your pure, functional data transformations with the messy, real world of side effects. Think of it as the membrane between your application's logic and everything outside it—the UI, the network, the file system, the console.
+
+## Starting Simple
+
+Let's see what reactive functions look like in practice:
+
+```python
+from fynx import reactive, observable
+
+count = observable(0)
+
+@reactive(count)
+def log_count(value):
+ print(f"Count: {value}")
+
+count.set(5) # Prints: "Count: 5"
+count.set(10) # Prints: "Count: 10"
+```
+
+The function runs automatically whenever `count` changes. You declare what should happen when data changes, and the framework handles the timing.
+
+## The Commitment: What You Gain and What You Give Up
+
+Once you decorate a function with `@reactive`, you're making a commitment. The function becomes automatic—it runs when its dependencies change. In exchange, you lose the ability to call it manually:
+
+```python
+@reactive(count)
+def log_count(value):
+ print(f"Count: {value}")
+
+log_count(10) # Raises fynx.reactive.ReactiveFunctionWasCalled exception
+```
+
+This isn't an arbitrary restriction. It's protecting you from confusion. If you could call `log_count()` manually *and* have it trigger automatically, which version of the value is authoritative? The manual call or the reactive update? The framework eliminates this ambiguity by enforcing one mode at a time.
+
+You can always change your mind, though. Call `.unsubscribe()` to sever the reactive connection and return the function to normal, non-reactive behavior:
+
+```python
+@reactive(count)
+def log_count(value):
+ print(f"Count: {value}")
+
+count.set(5) # Prints: "Count: 5"
+
+log_count.unsubscribe() # Severs the reactive connection
+
+count.set(10) # No output—the function is no longer reactive
+log_count(15) # Prints: "Count: 15"—now works as a normal function
+```
+
+After unsubscribing, the function reverts to its original, non-reactive form. You can call it manually again, and it will no longer respond to changes in its former dependencies.
+
+## A Crucial Detail: Initial State and Change Semantics
+
+Here's something that might surprise you: when you create a reactive function, it doesn't fire immediately with the current value. It only fires when the value *changes*.
+
+```python
+ready = observable(True) # Already true
+
+@reactive(ready)
+def on_ready(value):
+ print(f"Ready: {value}")
+
+# Nothing prints yet, even though ready is True
+
+ready.set(False) # Prints: "Ready: False"
+ready.set(True) # Prints: "Ready: True"
+```
+
+This behavior has deep roots in category theory—reactive functions form what's called a "pullback" in categorical semantics. The initial state isn't captured because you haven't pulled back through a change yet. You're observing the flow of changes, not the snapshot of current state.
+
+This matters enormously for initialization logic. If you need something to run immediately based on current state, you'll need to handle that separately. Reactive functions are about responding to transitions, not about reflecting static state.
+
+## Conditional Reactions: The MobX `when` Pattern
+
+Here's where things get powerful. You can combine observables with logical operators to create conditional reactions that only fire when specific conditions are met:
+
+```python
+is_logged_in = observable(False)
+has_data = observable(False)
+is_loading = observable(True)
+should_sync = observable(False)
+
+# React only when logged in AND has data AND NOT loading OR should sync
+@reactive(is_logged_in & has_data & ~is_loading + should_sync)
+def sync_to_server(should_run):
+ if should_run:
+ perform_sync()
+```
+
+The operators work as you'd expect:
+
+* `&` is logical AND
+* `+` is logical OR
+* `~` is logical NOT (negation)
+
+These create composite observables that emit values based on boolean logic applied to their constituent observables. The critical insight: the reaction still follows the change-only semantics. Even if your condition is `True` at the moment you attach the reactive function, it won't fire until something changes *and* the condition is met.
+
+```python
+logged_in = observable(True)
+verified = observable(True)
+
+# Even though both are already True, this doesn't fire yet
+@reactive(logged_in & verified)
+def enable_premium_features(both_true):
+ print("Premium features enabled")
+
+# Nothing printed yet
+
+logged_in.set(False) # Condition now False, triggers reaction
+# Prints: "Premium features enabled" with value False
+
+verified.set(False) # Both False, triggers reaction
+# Prints: "Premium features enabled" with value False
+
+logged_in.set(True) # One is True, one is False, triggers reaction
+# Prints: "Premium features enabled" with value False
+
+verified.set(True) # Both True now, triggers reaction
+# Prints: "Premium features enabled" with value True
+```
+
+This mirrors MobX's `when` behavior, but with more compositional flexibility. You're not limited to simple conditions—you can build arbitrarily complex boolean expressions that describe exactly when your side effect should consider running.
+
+## Multiple Dependencies Without Conditions
+
+Sometimes you just want a reaction to fire whenever any of several observables change, without boolean logic:
+
+```python
+name = observable("Alice")
+age = observable(30)
+
+# Derive a combined observable first
+full_name = (name + age) >> (lambda n, a: f"{n} ({a} years old)")
+
+# Then react to changes in the derivation
+@reactive(full_name)
+def update_display(display_name):
+ print(f"Display: {display_name}")
+
+name.set("Bob") # Triggers with "Bob (30 years old)"
+age.set(31) # Triggers with "Bob (31 years old)"
+```
+
+Notice the pattern: derive first, react second. The `+` operator here isn't doing boolean OR—it's combining observables into a tuple-like stream. The `>>` operator then transforms that stream. Only after you've created a derived observable do you attach the reaction.
+
+## The Core Insight: Where @reactive Belongs
+
+Here's the fundamental principle that makes reactive systems maintainable: **`@reactive` is for side effects, not for deriving state.**
+
+When you're tempted to use `@reactive`, ask yourself: "Am I computing a new value from existing data, or am I sending information outside my application?" If you're computing, you want `>>` or `+` operators. If you're communicating with the outside world, you want `@reactive`.
+
+This distinction creates what we call the "functional core, reactive shell" pattern. Your core is pure transformations—testable, predictable, composable. Your shell is reactions—the unavoidable side effects that make your application actually do something.
+
+Let's see this in a real example:
+
+```python
+# ===== FUNCTIONAL CORE (Pure) =====
+class OrderCore(Store):
+ items = observable([])
+ shipping_address = observable(None)
+ payment_method = observable(None)
+ is_processing = observable(False)
+
+ # Pure derivations—no side effects anywhere
+ subtotal = items >> (lambda i: sum(x['price'] * x['qty'] for x in i))
+ has_items = items >> (lambda i: len(i) > 0)
+ has_address = shipping_address >> (lambda a: a is not None)
+ has_payment = payment_method >> (lambda p: p is not None)
+
+ # Boolean logic for conditions
+ can_checkout = (has_items & has_address & has_payment & ~is_processing) >> (lambda x: x)
+
+ tax = subtotal >> (lambda s: s * 0.08)
+ total = (subtotal + tax) >> (lambda s, t: s + t)
+
+# ===== REACTIVE SHELL (Impure) =====
+@reactive(OrderCore.can_checkout)
+def update_checkout_button(can_checkout):
+ button.disabled = not can_checkout
+
+@reactive(OrderCore.total)
+def update_display(total):
+ render_total(f"${total:.2f}")
+
+# Only auto-save when we have items and aren't processing
+@reactive(OrderCore.has_items & ~OrderCore.is_processing)
+def auto_save(should_save):
+ if should_save:
+ save_to_db(OrderCore.to_dict())
+```
+
+Notice how the core is entirely composed of derivations—values computed from other values. No database calls, no DOM manipulation, no network requests. These pure transformations are easy to test, easy to understand, and easy to change.
+
+The reactions appear only at the boundary. They're where your perfect functional world meets reality: updating a button's state, rendering to the screen, persisting to a database. The conditional operators let you express exactly when these side effects should occur, without polluting your core logic.
+
+## The Trap of Clever Reactions
+
+The biggest pitfall with `@reactive` is trying to be too clever. Three patterns consistently cause problems:
+
+**The infinite loop.** When a reaction modifies what it's watching, you've created a feedback cycle:
+
+```python
+count = observable(0)
+
+@reactive(count)
+def increment_forever(value):
+ count.set(value + 1) # Every change triggers another change
+```
+
+This is obvious in toy examples but can hide in real code when the dependency is indirect. The change semantics don't save you here—each change triggers the reaction, which causes another change, ad infinitum.
+
+**The hidden cache.** When reactions maintain their own state, you've split your application's state across two systems:
+
+```python
+results_cache = {}
+
+@reactive(query)
+def update_cache(query_string):
+ results_cache[query_string] = fetch_results(query_string)
+```
+
+Now you have to remember that `results_cache` exists and keep it synchronized. Better to make the cache itself observable and derive from it.
+
+**The sequential assumption.** When reactions depend on each other's execution order, you've created fragile coupling:
+
+```python
+shared_list = []
+
+@reactive(data)
+def reaction_one(value):
+ shared_list.append(value)
+
+@reactive(data)
+def reaction_two(value):
+ # Assumes reaction_one has already run
+ print(f"List has {len(shared_list)} items")
+```
+
+The second reaction assumes the first has already run. But that's an implementation detail, not a guarantee. If execution order changes, your code breaks silently.
+
+The fix for all three is the same: keep reactions independent and stateless. Let the observable system coordinate state. Keep reactions purely about effects.
+
+## Advanced Patterns: Conditional Guards and Cleanup
+
+The conditional operators shine when you need to guard expensive or sensitive operations:
+
+```python
+user = observable(None)
+has_permission = observable(False)
+is_online = observable(False)
+
+# Only sync when user is logged in, has permission, and is online
+@reactive(user & has_permission & is_online)
+def sync_sensitive_data(should_sync):
+ if should_sync and user.get():
+ api.sync_user_data(user.get().id)
+
+# Later, when you want to stop syncing entirely:
+sync_sensitive_data.unsubscribe()
+```
+
+The unsubscribe mechanism becomes particularly important in cleanup scenarios. If your reactive function represents a resource that needs explicit teardown (like a WebSocket connection or a file handle), you can unsubscribe when you're done to prevent further reactions and then perform cleanup in the function itself.
+
+## Store-Level Reactions
+
+Stores collect related observables, and you can react to derived properties on stores just like standalone observables:
+
+```python
+class UserStore(Store):
+ name = observable("Alice")
+ age = observable(30)
+ is_active = observable(True)
+
+ user_summary = (name + age) >> (lambda n, a: f"{n}, {a}")
+ should_display = is_active & (age >> (lambda a: a >= 18))
+
+@reactive(UserStore.user_summary)
+def sync_to_server(summary):
+ api.post('/user/update', {'summary': summary})
+
+@reactive(UserStore.should_display)
+def toggle_profile_visibility(should_show):
+ profile_element.visible = should_show
+
+UserStore.name = "Bob" # Triggers first reaction
+UserStore.age = 31 # Triggers both reactions
+UserStore.is_active = False # Triggers second reaction only
+```
+
+The store becomes your functional core. The reactions watching it become your shell. This separation makes testing straightforward—test the store logic in isolation, mock the side effects in the reactions.
+
+***
+
+**The Big Picture:** Use `@reactive` sparingly. Most of your code should be pure derivations using `>>`, `+`, `&`, and `~`. Reactions appear only at the edges, where your application must interact with something external. The conditional operators let you express exactly when these interactions should happen without mixing conditions into your business logic. When you find yourself reaching for `@reactive`, pause and ask: "Is this really a side effect, or am I just deriving new state?" That question alone will guide you toward cleaner, more maintainable reactive systems.
+
+::: fynx.reactive
diff --git a/docs/generation/markdown/reference/store.md b/docs/generation/markdown/reference/store.md
new file mode 100644
index 0000000..e9d99fa
--- /dev/null
+++ b/docs/generation/markdown/reference/store.md
@@ -0,0 +1,5 @@
+# Store Class
+
+Container class for grouping observables and managing reactive state.
+
+::: fynx.store
diff --git a/docs/generation/markdown/store.md b/docs/generation/markdown/store.md
deleted file mode 100644
index 6e2a6c0..0000000
--- a/docs/generation/markdown/store.md
+++ /dev/null
@@ -1,5 +0,0 @@
-# Store Class & @observable Decorator
-
-Container class for grouping observables and the decorator for making class attributes reactive.
-
-::: fynx.store
diff --git a/docs/generation/markdown/conditionals.md b/docs/generation/markdown/tutorial/conditionals.md
similarity index 84%
rename from docs/generation/markdown/conditionals.md
rename to docs/generation/markdown/tutorial/conditionals.md
index 73c8e6b..e9299c5 100644
--- a/docs/generation/markdown/conditionals.md
+++ b/docs/generation/markdown/tutorial/conditionals.md
@@ -25,7 +25,7 @@ This works, but it's verbose. You have to write filtering logic in every subscri
## The Solution: Conditional Observables
-FynX gives you operators that create filtered, conditional observables. The key insight: separate the condition logic from the filtering operation.
+FynX gives you operators that create filtered, conditional observables. The insight: separate the condition logic from the filtering operation.
```python
temperature = observable(20)
@@ -143,6 +143,63 @@ is_online.set(False) # is_offline becomes True, prints: "User went offline"
is_online.set(True) # is_offline becomes False, no output
```
+## The | Operator: Logical OR
+
+The `|` operator creates logical OR conditions between boolean observables. It emits when ANY of the conditions is truthy:
+
+```python
+is_error = observable(False)
+is_warning = observable(True)
+is_critical = observable(False)
+
+# Logical OR using | operator
+needs_attention = is_error | is_warning | is_critical
+
+# Alternative using .either() method
+needs_attention_alt = is_error.either(is_warning).either(is_critical)
+
+needs_attention.subscribe(lambda needs_attention: {
+ if needs_attention:
+ print("⚠️ System needs attention!")
+})
+
+# Updates automatically when any condition changes
+is_error.set(True) # Prints: "⚠️ System needs attention!"
+is_warning.set(False) # Still prints (is_error is True)
+is_error.set(False) # Still prints (is_critical is False, but was True initially)
+```
+
+The `|` operator creates conditional observables that only emit when the OR result is truthy. If the initial OR result is falsy, it raises `ConditionalNeverMet`.
+
+### Combining OR with Other Operators
+
+You can combine `|` with `&` and `~` for complex logical expressions:
+
+```python
+user_input = observable("")
+is_admin = observable(False)
+is_moderator = observable(True)
+
+# Create boolean conditions
+has_input = user_input >> (lambda u: len(u) > 0)
+has_permission = is_admin | is_moderator # OR condition
+is_not_empty = has_input & (lambda h: h == True) # AND condition
+
+# Complex condition: user has input AND (is admin OR moderator)
+can_submit = user_input & has_input & has_permission
+
+can_submit.subscribe(lambda can_submit: {
+ if can_submit is not None:
+ print("✅ User can submit")
+ else:
+ print("❌ User cannot submit")
+})
+
+user_input.set("Hello") # Prints: "✅ User can submit"
+is_moderator.set(False) # Prints: "❌ User cannot submit" (no permission)
+is_admin.set(True) # Prints: "✅ User can submit" (admin permission)
+```
+
### Combining Negation with Filtering
Create "everything except" patterns:
@@ -402,11 +459,12 @@ form_valid = (email_ok & (lambda _: True)) & (password_ok & (lambda _: True))
Conditionals transform your reactive system from "process everything" to "process only what matters":
-- **`&` operator**: Filter data streams based on predicates
-- **`~` operator**: Invert boolean conditions
-- **Performance**: Skip unnecessary computations
-- **Clarity**: Separate filtering logic from reaction logic
-- **Composition**: Combine conditions with other operators
+* **`&` operator**: Filter data streams based on predicates
+* **`|` operator**: Create logical OR conditions between boolean observables
+* **`~` operator**: Invert boolean conditions
+* **Performance**: Skip unnecessary computations
+* **Clarity**: Separate filtering logic from reaction logic
+* **Composition**: Combine conditions with other operators
Think of conditionals as reactive filters. They let you create observables that only emit valuable data, reducing noise and improving performance. Combined with transformations (`>>`) and reactions (`@reactive`), they give you a complete toolkit for building sophisticated reactive applications.
diff --git a/docs/generation/markdown/derived-observables.md b/docs/generation/markdown/tutorial/derived-observables.md
similarity index 92%
rename from docs/generation/markdown/derived-observables.md
rename to docs/generation/markdown/tutorial/derived-observables.md
index d08e7b6..4753660 100644
--- a/docs/generation/markdown/derived-observables.md
+++ b/docs/generation/markdown/tutorial/derived-observables.md
@@ -1,4 +1,4 @@
-# Derived Observables: Transforming Data with `.then()` and `>>`
+# Transforming Data with `.then()` and `>>`
Observables hold reactive values, and conditionals filter them. But what truly unlocks FynX's power is transformation—the ability to derive new values from existing ones automatically.
@@ -74,7 +74,7 @@ def calculate_total(subtotal, tax, shipping):
subtotal = cart_items.then(calculate_subtotal)
tax = subtotal.then(calculate_tax)
shipping = subtotal.then(calculate_shipping)
-total = (subtotal | tax | shipping).then(calculate_total)
+total = (subtotal + tax + shipping).then(calculate_total)
# Subscribe to see results
def print_subtotal(s):
@@ -105,14 +105,15 @@ You declare what each value means in terms of others. Changes propagate automati
Both `.then()` and `>>` create computed observables, but with slightly different syntax:
-- **`.then()`**: `source_observable.then(transformation_function)` - Method syntax
-- **`>>`**: `source_observable >> transformation_function` - Operator syntax
+* **`.then()`**: `source_observable.then(transformation_function)` - Method syntax
+* **`>>`**: `source_observable >> transformation_function` - Operator syntax
Both approaches:
-- Take the current value from the source observable
-- Pass it to your transformation function immediately (eager evaluation)
-- Wrap the result in a new observable
-- Automatically re-run the transformation when the source changes
+
+* Take the current value from the source observable
+* Pass it to your transformation function immediately (eager evaluation)
+* Wrap the result in a new observable
+* Automatically re-run the transformation when the source changes
```python
numbers = observable([1, 2, 3])
@@ -171,15 +172,15 @@ def create_greeting(n):
greeting_method = name.then(create_greeting)
greeting_operator = name >> create_greeting
-# Multiple observables (using | first)
+# Multiple observables (using + first)
first = observable("John")
last = observable("Doe")
def combine_names(first_name, last_name):
return f"{first_name} {last_name}"
-full_name_method = (first | last).then(combine_names)
-full_name_operator = (first | last) >> combine_names
+full_name_method = (first + last).then(combine_names)
+full_name_operator = (first + last) >> combine_names
```
### Return Values
@@ -269,25 +270,25 @@ def format_expensive_message(total_and_is_expensive):
total, is_exp = total_and_is_expensive
return f"High-value order: ${total:.2f}"
-# Use | to combine, then transform
-discounted_total_method = (prices | discount_rate).then(calculate_discounted_total)
-discounted_total_operator = (prices | discount_rate) >> calculate_discounted_total
+# Use + to combine, then transform
+discounted_total_method = (prices + discount_rate).then(calculate_discounted_total)
+discounted_total_operator = (prices + discount_rate) >> calculate_discounted_total
# Use & for conditions, then format
is_expensive_method = discounted_total_method.then(is_expensive)
is_expensive_operator = discounted_total_method >> is_expensive
-expensive_message_method = (discounted_total_method | is_expensive_method).then(format_expensive_message)
-expensive_message_operator = (discounted_total_method | is_expensive_operator) >> format_expensive_message
+expensive_message_method = (discounted_total_method + is_expensive_method).then(format_expensive_message)
+expensive_message_operator = (discounted_total_method + is_expensive_operator) >> format_expensive_message
```
## Performance Characteristics
Derived observables are lazy and efficient:
-- **Memoization**: Results are cached until source values change
-- **Selective Updates**: Only recalculates when dependencies actually change
-- **No Redundant Work**: If a transformation result hasn't changed, downstream observers don't re-run
+* **Memoization**: Results are cached until source values change
+* **Selective Updates**: Only recalculates when dependencies actually change
+* **No Redundant Work**: If a transformation result hasn't changed, downstream observers don't re-run
```python
def slow_computation(data):
@@ -368,11 +369,11 @@ def double_count(c):
derived_method = count.then(double_count) # Pass count, not count.value
derived_operator = count >> double_count # Pass count, not count.value
-merged = first_name | last_name # Pass observables, not .value
+merged = first_name + last_name # Pass observables, not .value
filtered = items & is_valid # Pass observables, not .value
```
-The operators (`.then()`, `>>`, `|`, `&`, `~`) are designed to work with observables and maintain reactivity. When you pass `.value` to them, you're passing a static snapshot instead of a reactive stream.
+The operators (`.then()`, `>>`, `+`, `&`, `~`) are designed to work with observables and maintain reactivity. When you pass `.value` to them, you're passing a static snapshot instead of a reactive stream.
**Inside subscribers and reactive functions, `.value` is fine:**
@@ -649,15 +650,15 @@ is_ready_operator = app_state >> is_ready_state
Both `.then()` and `>>` transform FynX from a simple notification system into a powerful data transformation engine. You stop writing imperative update code and start declaring relationships:
-- **From**: "When X changes, update Y, then update Z"
-- **To**: "Y is a transformation of X, Z is a transformation of Y"
+* **From**: "When X changes, update Y, then update Z"
+* **To**: "Y is a transformation of X, Z is a transformation of Y"
This declarative approach eliminates entire categories of bugs:
-- **No stale data**: Derived values always reflect current source values
-- **No forgotten updates**: The reactive graph handles all propagation
-- **No manual synchronization**: Relationships are maintained automatically
+* **No stale data**: Derived values always reflect current source values
+* **No forgotten updates**: The reactive graph handles all propagation
+* **No manual synchronization**: Relationships are maintained automatically
-Combined with conditionals (`&`) and merging (`|`), derived observables give you a complete toolkit for building reactive data pipelines. You describe what your data should look like, and FynX ensures it stays that way.
+Combined with conditionals (`&`) and merging (`+`), derived observables give you a complete toolkit for building reactive data pipelines. You describe what your data should look like, and FynX ensures it stays that way.
The next step is organizing these reactive pieces into reusable units called **Stores**—the architectural pattern that brings everything together.
diff --git a/docs/generation/markdown/observables.md b/docs/generation/markdown/tutorial/observables.md
similarity index 88%
rename from docs/generation/markdown/observables.md
rename to docs/generation/markdown/tutorial/observables.md
index 0ed8bf3..3586376 100644
--- a/docs/generation/markdown/observables.md
+++ b/docs/generation/markdown/tutorial/observables.md
@@ -186,7 +186,7 @@ base_price = observable(100)
quantity = observable(2)
# This creates a computed observable (we'll explore these deeply in the next section)
-total = (base_price | quantity) >> (lambda price, qty: price * qty)
+total = (base_price + quantity) >> (lambda price, qty: price * qty)
total.subscribe(lambda t: print(f"Total: ${t}"))
@@ -202,24 +202,24 @@ This is what observables enable: **declarative state management**. You describe
Observables shine in situations where:
-- **Multiple things depend on the same state** — One change needs to update several downstream systems
-- **State changes frequently** — User interactions, real-time data, animated values
-- **Dependencies are complex** — Value A depends on B and C, which depend on D and E
-- **You want to avoid manual synchronization** — Eliminating update code reduces bugs
+* **Multiple things depend on the same state** — One change needs to update several downstream systems
+* **State changes frequently** — User interactions, real-time data, animated values
+* **Dependencies are complex** — Value A depends on B and C, which depend on D and E
+* **You want to avoid manual synchronization** — Eliminating update code reduces bugs
Observables add overhead compared to plain variables. For simple scripts or one-off calculations, that overhead isn't worth it. But for interactive applications, data pipelines, or anything with non-trivial state management, observables pay for themselves quickly.
-
## What's Next
Observables are more than containers—they're nodes in a reactive graph. But standalone observables are just the beginning. The real power emerges when you learn to:
-- **Transform observables** using the `>>` operator to create derived values that update automatically
-- **Combine observables** using the `|` operator to work with multiple sources of data
-- **Filter observables** using the `&` operator to apply conditional logic and control when data flows
-- **Organize observables** into Stores for cleaner application architecture
-- **Automate reactions** with decorators that eliminate subscription boilerplate
+* **Transform observables** using the `>>` operator to create derived values that update automatically
+* **Combine observables** using the `+` operator to work with multiple sources of data
+* **Filter observables** using the `&` operator to apply conditional logic and control when data flows
+* **Create logical OR conditions** using the `|` operator to combine boolean observables
+* **Organize observables** into Stores for cleaner application architecture
+* **Automate reactions** with decorators that eliminate subscription boilerplate
Each of these builds on the foundation you've just learned. Observables are simple, but their composition creates sophisticated reactive systems.
-The key insight to carry forward: **observables aren't just containers—they're nodes in a reactive graph**. When you change one node, effects ripple through the entire structure automatically. That's the power FynX gives you.
+The insight to carry forward: **observables aren't just containers—they're nodes in a reactive graph**. When you change one node, effects ripple through the entire structure automatically. That's the power FynX gives you.
diff --git a/docs/generation/markdown/stores.md b/docs/generation/markdown/tutorial/stores.md
similarity index 93%
rename from docs/generation/markdown/stores.md
rename to docs/generation/markdown/tutorial/stores.md
index 1e57797..46afa89 100644
--- a/docs/generation/markdown/stores.md
+++ b/docs/generation/markdown/tutorial/stores.md
@@ -34,7 +34,7 @@ CounterStore.count.subscribe(lambda c: print(f"Count: {c}"))
CounterStore.count = 10 # Prints: "Count: 10"
```
-Notice the asymmetry: you read with direct access (`CounterStore.count`), but the value is still an observable. You can still subscribe to it, transform it with `>>`, merge it with `|`. The Store class uses Python descriptors to give you clean syntax while preserving all of observable's power.
+Notice the asymmetry: you read with direct access (`CounterStore.count`), but the value is still an observable. You can still subscribe to it, transform it with `>>`, merge it with `+`. The Store class uses Python descriptors to give you clean syntax while preserving all of observable's power.
## Why Stores Matter
@@ -160,7 +160,7 @@ Computed values memoize their results. After the first access, they return the c
## Combining Multiple Observables
-Most computed values depend on more than one observable. Use the `|` operator to merge observables:
+Most computed values depend on more than one observable. Use the `+` operator to merge observables:
```python
class CartStore(Store):
@@ -172,17 +172,17 @@ class CartStore(Store):
)
# Merge subtotal and tax_rate
- tax_amount = (subtotal | tax_rate) >> (
+ tax_amount = (subtotal + tax_rate) >> (
lambda sub, rate: sub * rate
)
# Merge subtotal and tax_amount
- total = (subtotal | tax_amount) >> (
+ total = (subtotal + tax_amount) >> (
lambda sub, tax: sub + tax
)
```
-The `|` operator creates a merged observable that emits a tuple. When you transform it with `>>`, the function receives one argument per observable:
+The `+` operator creates a merged observable that emits a tuple. When you transform it with `>>`, the function receives one argument per observable:
```python
CartStore.items = [{'name': 'Widget', 'price': 20, 'quantity': 1}]
@@ -196,7 +196,7 @@ print(CartStore.tax_amount) # 2.0 (recalculated)
print(CartStore.total) # 22.0 (recalculated)
```
-Any change to a merged observable triggers recomputation. This makes `|` perfect for values that need to coordinate multiple pieces of state.
+Any change to a merged observable triggers recomputation. This makes `+` perfect for values that need to coordinate multiple pieces of state.
## Methods: Encapsulating State Changes
@@ -296,12 +296,12 @@ class AnalyticsStore(Store):
total = values >> (lambda v: sum(v))
# Level 2: Depends on count and total
- mean = (total | count) >> (
+ mean = (total + count) >> (
lambda t, c: t / c if c > 0 else 0
)
# Level 3: Depends on values and mean
- variance = (values | mean | count) >> (
+ variance = (values + mean + count) >> (
lambda vals, avg, n: (
sum((x - avg) ** 2 for x in vals) / (n - 1) if n > 1 else 0
)
@@ -340,7 +340,7 @@ class UserProfileStore(Store):
is_premium = observable(False)
# Computed: full name
- full_name = (first_name | last_name) >> (
+ full_name = (first_name + last_name) >> (
lambda first, last: f"{first} {last}".strip()
)
@@ -358,13 +358,13 @@ class UserProfileStore(Store):
is_adult = age >> (lambda a: a >= 18)
# Computed: profile completeness
- is_complete = (first_name | last_name | email | is_email_valid) >> (
+ is_complete = (first_name + last_name + email + is_email_valid) >> (
lambda first, last, email_addr, email_valid:
bool(first and last and email_addr and email_valid)
)
# Computed: user tier
- user_tier = (is_premium | is_complete) >> (
+ user_tier = (is_premium + is_complete) >> (
lambda premium, complete: (
"Premium" if premium else
"Complete" if complete else
@@ -455,7 +455,7 @@ class UIStore(Store):
)
# Depends on multiple observables from ThemeStore
- css_vars = (ThemeStore.mode | ThemeStore.font_size) >> (
+ css_vars = (ThemeStore.mode + ThemeStore.font_size) >> (
lambda mode, size: {
'--bg': "#ffffff" if mode == "light" else "#1a1a1a",
'--text': "#000000" if mode == "light" else "#ffffff",
@@ -482,6 +482,7 @@ Each Store maintains its own domain, but computed values can reach across Store
Use Stores when you have:
**Related state that belongs together:**
+
```python
# Good: Cart-related state in CartStore
class CartStore(Store):
@@ -491,6 +492,7 @@ class CartStore(Store):
```
**State that needs derived values:**
+
```python
# Good: Computed values with their source state
class FormStore(Store):
@@ -499,10 +501,11 @@ class FormStore(Store):
email_valid = email >> (lambda e: '@' in e)
password_valid = password >> (lambda p: len(p) >= 8)
- form_valid = (email_valid | password_valid) >> (lambda e, p: e and p)
+ form_valid = (email_valid + password_valid) >> (lambda e, p: e and p)
```
**State that needs encapsulated modification:**
+
```python
# Good: Methods that maintain invariants
class AccountStore(Store):
@@ -552,9 +555,9 @@ print(ChildStore.count) # 10 (completely separate)
**Key Behavior:** Unlike standard Python inheritance where child classes share parent attributes, Store inheritance creates separate observable instances for each class. This ensures clean state isolation:
-- `BaseStore.count` and `ChildStore.count` are completely independent
-- Changes to one don't affect the other
-- Each class maintains its own reactive state
+* `BaseStore.count` and `ChildStore.count` are completely independent
+* Changes to one don't affect the other
+* Each class maintains its own reactive state
**Explicit Overrides:** You can still override inherited observables:
@@ -653,14 +656,14 @@ price = items >> (lambda items: sum(item['price'] for item in items))
Stores organize your reactive state into cohesive, testable units. They combine observables, computed values, and methods into structures that represent distinct domains of your application.
-Key concepts:
+Core concepts:
-- **Stores group related observables** — Keep state that belongs together in the same Store
-- **Observable descriptors enable clean syntax** — Read and write Store attributes naturally
-- **The `>>` operator creates computed values** — Derived state updates automatically
-- **The `|` operator merges observables** — Combine multiple sources for multi-input computations
-- **Always create new values** — Never mutate observable contents in place
-- **Methods encapsulate state changes** — Define clear APIs for modifying state
-- **Stores can depend on other Stores** — Build modular applications with cross-Store relationships
+* **Stores group related observables** — Keep state that belongs together in the same Store
+* **Observable descriptors enable clean syntax** — Read and write Store attributes naturally
+* **The `>>` operator creates computed values** — Derived state updates automatically
+* **The `+` operator merges observables** — Combine multiple sources for multi-input computations
+* **Always create new values** — Never mutate observable contents in place
+* **Methods encapsulate state changes** — Define clear APIs for modifying state
+* **Stores can depend on other Stores** — Build modular applications with cross-Store relationships
-With Stores, you can build reactive applications that scale from simple counters to complex, multi-domain state management systems. The key is organization: each Store owns its domain, exposes a clean API, and lets FynX handle all the synchronization automatically.
+With Stores, you can build reactive applications that scale from simple counters to complex, multi-domain state management systems. The secret is organization: each Store owns its domain, exposes a clean API, and lets FynX handle all the synchronization automatically.
diff --git a/docs/generation/markdown/tutorial/using-reactive.md b/docs/generation/markdown/tutorial/using-reactive.md
new file mode 100644
index 0000000..f8d2519
--- /dev/null
+++ b/docs/generation/markdown/tutorial/using-reactive.md
@@ -0,0 +1,824 @@
+# @reactive: Automatic Reactions to Change
+
+Observables hold state, and Stores organize it. But how do you actually respond when that state changes? How do you keep UI, databases, and external systems in sync?
+
+Right now, if you want to respond to changes, you write this:
+
+```python
+count = observable(0)
+
+def log_count(value):
+ print(f"Count: {value}")
+
+count.subscribe(log_count)
+```
+
+This works. But as your application grows, subscription management becomes tedious:
+
+```python
+# Subscriptions scattered everywhere
+count.subscribe(update_ui)
+count.subscribe(save_to_database)
+count.subscribe(notify_analytics)
+name.subscribe(update_greeting)
+email.subscribe(validate_email)
+(first_name + last_name).subscribe(update_display_name)
+
+# Later... did you remember to unsubscribe?
+count.unsubscribe(update_ui)
+# Wait, which function was subscribed to which observable?
+```
+
+You're back to manual synchronization, just with a different syntax. The subscriptions themselves become state you have to manage.
+
+There's a better way.
+
+## Introducing @reactive
+
+The `@reactive` decorator turns functions into automatic reactions. Instead of manually subscribing, you declare *what observables matter* and FynX handles the rest:
+
+```python
+from fynx import observable, reactive
+
+count = observable(0)
+
+@reactive(count)
+def log_count(value):
+ print(f"Count: {value}")
+
+count.set(5) # Prints: "Count: 5"
+count.set(10) # Prints: "Count: 10"
+```
+
+That's it. No manual subscription. No cleanup to remember. Just a declaration: "this function reacts to this observable."
+
+The decorator does two things:
+
+1. **Subscribes automatically** — No need to call `.subscribe()`
+2. **Runs on every change** — Whenever the observable changes, the function runs with the new value
+
+This is the bridge from passive state management (observables and stores) to active behavior (side effects that respond to changes).
+
+## A Critical Detail: When Reactions Fire
+
+Here's something that might surprise you: when you create a reactive function, it doesn't fire immediately with the current value. It only fires when the value *changes*.
+
+```python
+ready = observable(True) # Already true
+
+@reactive(ready)
+def on_ready(value):
+ print(f"Ready: {value}")
+
+# Nothing prints yet, even though ready is True
+
+ready.set(False) # Prints: "Ready: False"
+ready.set(True) # Prints: "Ready: True"
+```
+
+This behavior has deep roots in category theory—reactive functions form what's called a "pullback" in categorical semantics. The initial state isn't captured because you haven't pulled back through a change yet. You're observing the flow of changes, not the snapshot of current state.
+
+This matters enormously for initialization logic. If you need something to run immediately based on current state, you'll need to handle that separately, perhaps by calling the function once manually before decorating it, or by setting up your initial state in a way that triggers the reaction. Reactive functions are about responding to transitions, not about reflecting static state.
+
+Understanding this execution model is crucial:
+
+```python
+count = observable(0)
+
+@reactive(count)
+def log_count(value):
+ print(f"Count: {value}")
+
+# At this point, log_count has NOT run yet - no initial trigger
+
+count.set(5) # log_count runs for the first time
+# Output: "Count: 5"
+
+count.set(5) # Same value - does log_count run?
+# Output: (no additional output - only runs when value actually changes)
+```
+
+The function runs every time `.set()` is called with a different value—only when the value actually changes. The execution is synchronous—the function completes before `.set()` returns. This makes reactive code predictable and debuggable. When you write `count.set(5)`, you know that all reactive functions have finished by the time the next line runs.
+
+## The Mental Model: Declarative Side Effects
+
+Traditional programming separates "doing" from "reacting":
+
+```python
+# Traditional: Manual coordination
+def update_count(new_value):
+ count = new_value
+ update_ui(count) # Remember to call this
+ save_to_database(count) # Remember to call this
+ log_change(count) # Remember to call this
+```
+
+Every time you modify state, you must remember all the dependent actions. Miss one and your application falls out of sync.
+
+With `@reactive`, you declare the relationships once:
+
+```python
+# Reactive: Declare what should happen
+@reactive(count)
+def update_ui(value):
+ print(f"UI: {value}")
+
+@reactive(count)
+def save_to_database(value):
+ print(f"Saving: {value}")
+
+@reactive(count)
+def log_change(value):
+ print(f"Log: {value}")
+
+# Now just update state
+count.set(42)
+# All three functions run automatically
+# UI: 42
+# Saving: 42
+# Log: 42
+```
+
+You've moved from "remember to update everything" to "declare what should stay synchronized." The burden of coordination shifts from you to FynX.
+
+## Conditional Reactions: Boolean Logic for When to React
+
+Here's where `@reactive` becomes truly powerful. You can combine observables with logical operators to create conditional reactions that only fire when specific conditions are met:
+
+```python
+is_logged_in = observable(False)
+has_data = observable(False)
+is_loading = observable(True)
+should_sync = observable(False)
+
+# React only when logged in AND has data AND NOT loading OR should sync
+@reactive(is_logged_in & has_data & ~is_loading + should_sync)
+def sync_to_server(should_run):
+ if should_run:
+ perform_sync()
+```
+
+The operators work exactly as you'd expect:
+
+* `&` is logical AND
+* `+` is logical OR (when used with observables on both sides)
+* `~` is logical NOT (negation)
+
+These create composite observables that emit values based on boolean logic. The critical insight: the reaction still follows the change-only semantics. Even if your condition is `True` when you attach the reactive function, it won't fire until something changes *and* the condition evaluates.
+
+```python
+logged_in = observable(True)
+verified = observable(True)
+
+# Even though both are already True, this doesn't fire yet
+@reactive(logged_in & verified)
+def enable_premium_features(both_true):
+ print(f"Premium features: {both_true}")
+
+# Nothing printed yet - waiting for first change
+
+logged_in.set(False) # Condition now False, triggers reaction
+# Prints: "Premium features: False"
+
+verified.set(False) # Both False, triggers reaction
+# Prints: "Premium features: False"
+
+logged_in.set(True) # One is True, one is False, triggers reaction
+# Prints: "Premium features: False"
+
+verified.set(True) # Both True now, triggers reaction
+# Prints: "Premium features: True"
+```
+
+This mirrors MobX's `when` behavior, but with more compositional flexibility. You're not limited to simple conditions—you can build arbitrarily complex boolean expressions that describe exactly when your side effect should consider running.
+
+Think of it as event-driven reactions with declarative conditions. Instead of checking conditions inside your reaction function, you express them in the observable composition itself:
+
+```python
+# Instead of this:
+@reactive(status)
+def maybe_sync(status):
+ if status.logged_in and status.has_data and not status.loading:
+ perform_sync()
+
+# You can write this:
+@reactive(logged_in & has_data & ~is_loading)
+def sync_when_ready(should_sync):
+ if should_sync:
+ perform_sync()
+```
+
+The second version is clearer about *when* the sync happens—the condition is part of the observable dependency declaration, not buried in the function body.
+
+## Reacting to Multiple Observables
+
+Most real-world reactions depend on multiple pieces of state. When you need values from several observables without boolean logic, use the `+` operator differently—for combining observables into value tuples:
+
+```python
+first_name = observable("Alice")
+last_name = observable("Smith")
+
+# Derive a combined observable first
+full_name = (first_name + last_name) >> (lambda f, l: f"{f} {l}")
+
+# Then react to changes in the derivation
+@reactive(full_name)
+def update_display(display_name):
+ print(f"Display: {display_name}")
+
+# Nothing prints yet - waiting for first change
+
+first_name.set("Bob") # Triggers with "Bob Smith"
+last_name.set("Jones") # Triggers with "Bob Jones"
+```
+
+Notice the pattern: derive first, react second. The `+` operator combines observables into a stream of value pairs. The `>>` operator transforms that stream. Only after you've created a derived observable do you attach the reaction.
+
+This is a fundamental principle: most of the time, you don't react directly to raw observables. You react to *derived* observables—computed values that represent the meaningful state for your side effect.
+
+```python
+class CartStore(Store):
+ items = observable([])
+ tax_rate = observable(0.08)
+
+# Derive the meaningful state
+total = (CartStore.items + CartStore.tax_rate) >> (
+ lambda items, rate: sum(item['price'] * item['qty'] for item in items) * (1 + rate)
+)
+
+# React to the derived state
+@reactive(total)
+def update_total_display(total_amount):
+ print(f"Total: ${total_amount:.2f}")
+```
+
+The reaction only cares about the final computed total, not about whether items changed or tax rate changed. This separation of concerns—derive meaning, then react to it—keeps your reactive functions simple and focused.
+
+## Reacting to Entire Stores
+
+Sometimes you want to react to *any* change in a Store, regardless of which specific observable changed. Pass the Store class itself:
+
+```python
+class UserStore(Store):
+ name = observable("Alice")
+ age = observable(30)
+ email = observable("alice@example.com")
+
+@reactive(UserStore)
+def sync_to_server(store_snapshot):
+ print(f"Syncing: {store_snapshot.name}, {store_snapshot.email}")
+
+# Doesn't run immediately - waits for first change
+
+UserStore.name = "Bob" # Triggers reaction
+# Prints: "Syncing: Bob, alice@example.com"
+
+UserStore.age = 31 # Triggers reaction
+# Prints: "Syncing: Bob, alice@example.com"
+
+UserStore.email = "bob@example.com" # Triggers reaction
+# Prints: "Syncing: Bob, bob@example.com"
+```
+
+The function receives a snapshot of the entire Store. This is perfect for operations that need to consider the complete state—saving to a database, logging changes, synchronizing with a server.
+
+Note the subtle difference: when reacting to individual observables, you get the *values* as arguments. When reacting to a Store, you get the *Store snapshot itself* as a single argument, and you access observables through it.
+
+## Reacting to Computed Observables
+
+Everything that's an observable—including computed ones—works with `@reactive`:
+
+```python
+class CartStore(Store):
+ items = observable([])
+
+# Computed observable
+item_count = CartStore.items >> (lambda items: len(items))
+
+@reactive(item_count)
+def update_badge(count):
+ print(f"Cart badge: {count}")
+
+# Doesn't run immediately - waits for first change
+
+CartStore.items = [{'name': 'Widget', 'price': 10}]
+# Computed value recalculates: 1
+# Reactive function runs: "Cart badge: 1"
+
+CartStore.items = CartStore.items + [{'name': 'Gadget', 'price': 15}]
+# Computed value recalculates: 2
+# Reactive function runs: "Cart badge: 2"
+```
+
+You don't react to `CartStore.items` directly. You react to the *computed* value. This is powerful: it means you only care about changes in the *derived* state, not every modification to the underlying data.
+
+If the computed value doesn't change, the reaction doesn't fire:
+
+```python
+items = observable([1, 2, 3])
+length = items >> (lambda i: len(i))
+
+@reactive(length)
+def log_length(l):
+ print(f"Length: {l}")
+
+items.set([4, 5, 6]) # Length is still 3, reaction doesn't fire
+items.set([7, 8, 9, 10]) # Length is now 4, reaction fires
+```
+
+This is exactly what you want—reactions tied to semantic meaning, not raw data changes.
+
+## The Commitment: What You Gain and What You Give Up
+
+Once you decorate a function with `@reactive`, you're making a commitment. The function becomes automatic—it runs when its dependencies change. In exchange, you lose the ability to call it manually:
+
+```python
+@reactive(count)
+def log_count(value):
+ print(f"Count: {value}")
+
+log_count(10) # Raises fynx.reactive.ReactiveFunctionWasCalled exception
+```
+
+This isn't an arbitrary restriction. It's protecting you from confusion. If you could call `log_count()` manually *and* have it trigger automatically, which version of the value is authoritative? The manual call or the reactive update? The framework eliminates this ambiguity by enforcing one mode at a time.
+
+You can always change your mind, though. Call `.unsubscribe()` to sever the reactive connection and return the function to normal, non-reactive behavior:
+
+```python
+@reactive(count)
+def log_count(value):
+ print(f"Count: {value}")
+
+count.set(5) # Prints: "Count: 5"
+
+log_count.unsubscribe() # Severs the reactive connection
+
+count.set(10) # No output—the function is no longer reactive
+log_count(15) # Prints: "Count: 15"—now works as a normal function
+```
+
+After unsubscribing, the function reverts to its original, non-reactive form. You can call it manually again, and it will no longer respond to changes in its former dependencies.
+
+This lifecycle management is particularly important for component-based architectures:
+
+```python
+class UIComponent:
+ def __init__(self):
+ self.count = observable(0)
+
+ @reactive(self.count)
+ def update_display(value):
+ print(f"Display: {value}")
+
+ self._update_display = update_display
+
+ def destroy(self):
+ # Clean up when component is destroyed
+ self._update_display.unsubscribe()
+```
+
+The pattern is simple: create reactive functions when you need them, unsubscribe when you're done. This prevents memory leaks and ensures reactions don't outlive the components they serve.
+
+## Practical Example: Form Validation
+
+Here's where `@reactive` really shines—coordinating complex UI behavior:
+
+```python
+class FormStore(Store):
+ email = observable("")
+ password = observable("")
+ confirm_password = observable("")
+
+# Computed validations
+email_valid = FormStore.email >> (
+ lambda e: '@' in e and '.' in e.split('@')[-1]
+)
+
+password_valid = FormStore.password >> (
+ lambda p: len(p) >= 8
+)
+
+passwords_match = (FormStore.password + FormStore.confirm_password) >> (
+ lambda pwd, confirm: pwd == confirm and pwd != ""
+)
+
+form_valid = (email_valid & password_valid & passwords_match) >> (lambda x: x)
+
+# Reactive UI updates
+@reactive(email_valid)
+def update_email_indicator(is_valid):
+ status = "✓" if is_valid else "✗"
+ print(f"Email: {status}")
+
+@reactive(password_valid)
+def update_password_indicator(is_valid):
+ status = "✓" if is_valid else "✗"
+ print(f"Password strength: {status}")
+
+@reactive(passwords_match)
+def update_match_indicator(match):
+ status = "✓" if match else "✗"
+ print(f"Passwords match: {status}")
+
+@reactive(form_valid)
+def update_submit_button(is_valid):
+ state = "enabled" if is_valid else "disabled"
+ print(f"Submit button: {state}")
+
+# Reactive functions don't run immediately
+# Now update the form fields:
+FormStore.email = "alice@example.com"
+# Email: ✓ (email indicator runs)
+
+FormStore.password = "secure123"
+# Password strength: ✓ (password indicator runs)
+# Passwords match: ✗ (match indicator runs - passwords don't match yet)
+
+FormStore.confirm_password = "secure123"
+# Passwords match: ✓ (match indicator runs)
+# Submit button: enabled (form becomes valid)
+```
+
+Every UI element updates automatically in response to the relevant state changes. You never write "when email changes, check if it's valid and update the indicator." You just declare the relationship and FynX handles the orchestration.
+
+Notice how we use the `&` operator to create `form_valid`—it only becomes true when all three conditions are met. The reactive function on `form_valid` only fires when something changes, giving you precise control over when the submit button updates.
+
+## The Core Insight: Where @reactive Belongs
+
+Here's the fundamental principle that makes reactive systems maintainable: **`@reactive` is for side effects, not for deriving state.**
+
+When you're tempted to use `@reactive`, ask yourself: "Am I computing a new value from existing data, or am I sending information outside my application?" If you're computing, you want `>>`, `+`, `&`, or `~` operators. If you're communicating with the outside world, you want `@reactive`.
+
+This distinction creates what we call the "functional core, reactive shell" pattern. Your core is pure transformations—testable, predictable, composable. Your shell is reactions—the unavoidable side effects that make your application actually do something.
+
+```python
+# ===== FUNCTIONAL CORE (Pure) =====
+class OrderCore(Store):
+ items = observable([])
+ shipping_address = observable(None)
+ payment_method = observable(None)
+ is_processing = observable(False)
+
+ # Pure derivations—no side effects anywhere
+ subtotal = items >> (lambda i: sum(x['price'] * x['qty'] for x in i))
+ has_items = items >> (lambda i: len(i) > 0)
+ has_address = shipping_address >> (lambda a: a is not None)
+ has_payment = payment_method >> (lambda p: p is not None)
+
+ # Boolean logic for conditions
+ can_checkout = (has_items & has_address & has_payment & ~is_processing) >> (lambda x: x)
+
+ tax = subtotal >> (lambda s: s * 0.08)
+ total = (subtotal + tax) >> (lambda s, t: s + t)
+
+# ===== REACTIVE SHELL (Impure) =====
+@reactive(OrderCore.can_checkout)
+def update_checkout_button(can_checkout):
+ button.disabled = not can_checkout
+
+@reactive(OrderCore.total)
+def update_display(total):
+ render_total(f"${total:.2f}")
+
+# Only auto-save when we have items and aren't processing
+@reactive(OrderCore.has_items & ~OrderCore.is_processing)
+def auto_save(should_save):
+ if should_save:
+ save_to_db(OrderCore.to_dict())
+```
+
+Notice how the core is entirely composed of derivations—values computed from other values. No database calls, no DOM manipulation, no network requests. These pure transformations are easy to test, easy to understand, and easy to change.
+
+The reactions appear only at the boundary. They're where your perfect functional world meets reality: updating a button's state, rendering to the screen, persisting to a database. The conditional operators let you express exactly when these side effects should occur, without polluting your core logic.
+
+## Best Practices for @reactive
+
+### Use @reactive for Side Effects Only
+
+**✅ USE @reactive for:**
+
+**Side Effects at Application Boundaries**
+
+```python
+@reactive(UserStore)
+def sync_to_server(store):
+ # Network I/O
+ api.post('/user/update', store.to_dict())
+
+@reactive(settings_changed)
+def save_to_local_storage(settings):
+ # Browser storage I/O
+ localStorage.setItem('settings', json.dumps(settings))
+```
+
+**UI Updates** (DOM manipulation, rendering)
+
+```python
+@reactive(cart_total)
+def update_total_display(total):
+ # DOM manipulation
+ render_total(f"${total:.2f}")
+```
+
+**Logging and Monitoring**
+
+```python
+@reactive(error_observable)
+def log_errors(error):
+ # Logging side effect
+ logger.error(f"Application error: {error}")
+ analytics.track('error', {'message': str(error)})
+```
+
+**Cross-System Coordination** (when one reactive system needs to update another)
+
+```python
+@reactive(ThemeStore.dark_mode)
+def update_editor_theme(is_dark):
+ # Coordinating separate systems
+ EditorStore.theme = 'dark' if is_dark else 'light'
+```
+
+**❌ AVOID @reactive for:**
+
+**Deriving State** (use `>>`, `+`, `&`, `~` instead)
+
+```python
+# BAD: Using @reactive for transformation
+@reactive(count)
+def doubled_count(value):
+ doubled.set(value * 2) # Modifying another observable
+
+# GOOD: Use functional transformation
+doubled = count >> (lambda x: x * 2)
+```
+
+**State Coordination** (use computed observables)
+
+```python
+# BAD: Coordinating state in reactive functions
+@reactive(first_name, last_name)
+def update_full_name(first, last):
+ full_name.set(f"{first} {last}")
+
+# GOOD: Express as derived state
+full_name = (first_name + last_name) >> (lambda f, l: f"{f} {l}")
+```
+
+**Business Logic** (keep logic in pure transformations)
+
+```python
+# BAD: Business logic in reactive function
+@reactive(order_items)
+def calculate_total(items):
+ total = sum(item.price * item.quantity for item in items)
+ tax = total * 0.08
+ order_total.set(total + tax)
+
+# GOOD: Business logic in pure transformation
+order_total = order_items >> (lambda items:
+ sum(i.price * i.quantity for i in items) * 1.08
+)
+```
+
+### Anti-Patterns to Avoid
+
+**The infinite loop.** When a reaction modifies what it's watching, you've created a feedback cycle:
+
+```python
+count = observable(0)
+
+@reactive(count)
+def increment_forever(value):
+ count.set(value + 1) # Every change triggers another change
+```
+
+This is obvious in toy examples but can hide in real code when the dependency is indirect. The change semantics don't save you here—each change triggers the reaction, which causes another change, ad infinitum.
+
+**The hidden cache.** When reactions maintain their own state, you've split your application's state across two systems:
+
+```python
+results_cache = {}
+
+@reactive(query)
+def update_cache(query_string):
+ results_cache[query_string] = fetch_results(query_string)
+```
+
+Now you have to remember that `results_cache` exists and keep it synchronized. Better to make the cache itself observable and derive from it.
+
+**The sequential assumption.** When reactions depend on each other's execution order, you've created fragile coupling:
+
+```python
+shared_list = []
+
+@reactive(data)
+def reaction_one(value):
+ shared_list.append(value)
+
+@reactive(data)
+def reaction_two(value):
+ # Assumes reaction_one has already run
+ print(f"List has {len(shared_list)} items")
+```
+
+The second reaction assumes the first has already run. But that's an implementation detail, not a guarantee. If execution order changes, your code breaks silently.
+
+The fix for all three is the same: keep reactions independent and stateless. Let the observable system coordinate state. Keep reactions purely about effects.
+
+## Advanced Patterns: Conditional Guards and Cleanup
+
+The conditional operators shine when you need to guard expensive or sensitive operations:
+
+```python
+user = observable(None)
+has_permission = observable(False)
+is_online = observable(False)
+
+# Only sync when user is logged in, has permission, and is online
+@reactive(user & has_permission & is_online)
+def sync_sensitive_data(should_sync):
+ if should_sync and user.get():
+ api.sync_user_data(user.get().id)
+
+# Later, when you want to stop syncing entirely:
+sync_sensitive_data.unsubscribe()
+```
+
+The unsubscribe mechanism becomes particularly important in cleanup scenarios. If your reactive function represents a resource that needs explicit teardown (like a WebSocket connection or a file handle), you can unsubscribe when you're done to prevent further reactions and then perform cleanup in the function itself.
+
+## @reactive vs. Manual Subscriptions
+
+When should you use `@reactive` instead of calling `.subscribe()` directly?
+
+**Use `@reactive` when:**
+
+You want declarative, self-documenting code:
+
+```python
+@reactive(user_count)
+def update_dashboard(count):
+ print(f"Users: {count}")
+```
+
+You're defining reactions at module level or class definition:
+
+```python
+class UIController:
+ @reactive(AppStore.mode)
+ def sync_mode(mode):
+ update_ui_mode(mode)
+```
+
+You need lifecycle management with unsubscribe capability:
+
+```python
+@reactive(data_stream)
+def process_data(data):
+ handle_data(data)
+
+# Later, when component is destroyed
+process_data.unsubscribe() # Clean up
+```
+
+**Use `.subscribe()` when:**
+
+You need dynamic subscriptions that change at runtime:
+
+```python
+if user_wants_notifications:
+ count.subscribe(send_notification)
+```
+
+You need to unsubscribe conditionally:
+
+```python
+subscription_func = count.subscribe(handler)
+if some_condition:
+ count.unsubscribe(subscription_func)
+```
+
+You're building a library that accepts observables:
+
+```python
+def create_widget(data_observable):
+ data_observable.subscribe(widget.update)
+```
+
+The rule of thumb: `@reactive` for static, declarative reactions with optional cleanup via `.unsubscribe()`. `.subscribe()` for dynamic, programmatic subscriptions that you manage explicitly.
+
+## Gotchas and Edge Cases
+
+**Infinite loops are possible**
+
+```python
+# BAD: Modifying what you're watching
+count = observable(0)
+
+@reactive(count)
+def increment_forever(value):
+ count.set(value + 1) # DON'T DO THIS
+```
+
+Solution: Reactive functions should perform *side effects*, not modify the observables they're watching. Use computed observables for transformations.
+
+**Reactive functions don't track .get() or .value reads**
+
+```python
+# BAD: Hidden dependency
+other_count = observable(10)
+
+@reactive(count)
+def show_sum(value):
+ print(f"Sum: {value + other_count.get()}") # Hidden dependency
+
+count.set(5) # Prints: "Sum: 15"
+other_count.set(20) # Doesn't trigger show_sum - bug!
+```
+
+Solution: Make all dependencies explicit in the decorator.
+
+**Reactive functions receive values, not observables**
+
+```python
+# BAD: Trying to modify the value parameter
+@reactive(count)
+def try_to_modify(value):
+ value.set(100) # ERROR: value is an int, not an observable
+```
+
+Solution: Access the observable directly if you need to modify it (though you usually shouldn't).
+
+**Store reactions receive snapshots**
+
+```python
+@reactive(UserStore)
+def save_user(store):
+ # store is a snapshot of UserStore at this moment
+ # store.name is the current value, not an observable
+ save_to_db(store.name) # Correct
+
+ # This won't work:
+ store.name.subscribe(handler) # ERROR
+```
+
+**Order-dependent reactions are fragile**
+
+Express dependencies through computed observables instead of relying on execution order between multiple reactions.
+
+## Performance Considerations
+
+Reactive functions run synchronously on every change. For expensive operations, consider:
+
+**Reacting to derived state that filters changes:**
+
+```python
+search_query = observable("")
+
+# Only changes when meaningful
+filtered_results = search_query >> (
+ lambda q: search_database(q) if len(q) >= 3 else []
+)
+
+@reactive(filtered_results)
+def update_ui(results):
+ display_results(results)
+```
+
+**Using conditional observables to limit when reactions fire:**
+
+```python
+should_update = (user_active & ~is_loading) >> (lambda x: x)
+
+@reactive(should_update)
+def update_display(should):
+ if should:
+ expensive_render()
+```
+
+**Conditional logic inside reactions:**
+
+```python
+@reactive(mouse_position)
+def update_tooltip(position):
+ if should_show_tooltip(position):
+ expensive_tooltip_render(position)
+```
+
+## Summary
+
+The `@reactive` decorator transforms functions into automatic reactions that run whenever observables change:
+
+* **Declarative subscriptions** — No manual `.subscribe()` calls to manage
+* **Runs on changes only** — No initial execution; waits for first change (pullback semantics)
+* **Works with any observable** — Standalone, Store attributes, computed values, merged observables
+* **Boolean operators for conditions** — Use `&`, `+`, `~` to create conditional reactions (like MobX's `when`)
+* **Multiple observable support** — Derive combined observables first, then react
+* **Store-level reactions** — React to any change in an entire Store
+* **Lifecycle management** — Use `.unsubscribe()` to stop reactive behavior and restore normal function calls
+* **Prevents manual calls** — Raises `fynx.reactive.ReactiveFunctionWasCalled` if called manually while subscribed
+* **Side effects, not state changes** — Reactive functions should perform effects, not modify watched observables
+
+With `@reactive`, you declare *what should happen* when state changes. FynX ensures it happens automatically, in the right order, every time. This eliminates a whole category of synchronization bugs and makes your reactive systems self-maintaining.
+
+The rule of thumb here is that most of your code should be pure derivations using `>>`, `+`, `&`, and `~`. Reactions with `@reactive` appear only at the edges, where your application must interact with something external. This separation—the functional core, reactive shell pattern—is what makes reactive systems both powerful and maintainable.
diff --git a/docs/generation/markdown/using-reactive.md b/docs/generation/markdown/using-reactive.md
deleted file mode 100644
index 4e0e748..0000000
--- a/docs/generation/markdown/using-reactive.md
+++ /dev/null
@@ -1,548 +0,0 @@
-# @reactive: Automatic Reactions to Change
-
-Observables hold state, and Stores organize it. But how do you actually respond when that state changes? How do you keep UI, databases, and external systems in sync?
-
-Right now, if you want to respond to changes, you write this:
-
-```python
-count = observable(0)
-
-def log_count(value):
- print(f"Count: {value}")
-
-count.subscribe(log_count)
-```
-
-This works. But as your application grows, subscription management becomes tedious:
-
-```python
-# Subscriptions scattered everywhere
-count.subscribe(update_ui)
-count.subscribe(save_to_database)
-count.subscribe(notify_analytics)
-name.subscribe(update_greeting)
-email.subscribe(validate_email)
-(first_name | last_name).subscribe(update_display_name)
-
-# Later... did you remember to unsubscribe?
-count.unsubscribe(update_ui)
-# Wait, which function was subscribed to which observable?
-```
-
-You're back to manual synchronization, just with a different syntax. The subscriptions themselves become state you have to manage.
-
-There's a better way.
-
-## Introducing @reactive
-
-The `@reactive` decorator turns functions into automatic reactions. Instead of manually subscribing, you declare *what observables matter* and FynX handles the rest:
-
-```python
-from fynx import observable, reactive
-
-count = observable(0)
-
-@reactive(count)
-def log_count(value):
- print(f"Count: {value}")
-
-count.set(5) # Prints: "Count: 5"
-count.set(10) # Prints: "Count: 10"
-```
-
-That's it. No manual subscription. No cleanup. Just a declaration: "this function reacts to this observable."
-
-The decorator does three things:
-
-1. **Subscribes automatically** — No need to call `.subscribe()`
-2. **Runs immediately** — The function executes once when decorated, giving you the initial state
-3. **Runs on every change** — Whenever the observable changes, the function runs with the new value
-
-This is the bridge from passive state management (observables and stores) to active behavior (side effects that respond to changes).
-
-## How It Works: The Execution Model
-
-Understanding when `@reactive` functions run is crucial:
-
-```python
-count = observable(0)
-
-@reactive(count)
-def log_count(value):
- print(f"Count: {value}")
-
-# At this point, log_count has already run once with the initial value (0)
-# Output so far: "Count: 0"
-
-count.set(5) # log_count runs again
-# Output: "Count: 5"
-
-count.set(5) # Same value - does log_count run?
-# Output: (no additional output - only runs when value actually changes)
-```
-
-The function runs:
-- **Every time `.set()` is called with a different value** — Only when the value actually changes
-- **Synchronously** — The function completes before `.set()` returns
-
-This synchronous execution is important. When you write `count.set(5)`, you know that all reactive functions have finished by the time the next line of code runs. This makes reactive code predictable and debuggable.
-
-## The Mental Model: Declarative Side Effects
-
-Traditional programming separates "doing" from "reacting":
-
-```python
-# Traditional: Manual coordination
-def update_count(new_value):
- count = new_value
- update_ui(count) # Remember to call this
- save_to_database(count) # Remember to call this
- log_change(count) # Remember to call this
-```
-
-Every time you modify state, you must remember all the dependent actions. Miss one and your application falls out of sync.
-
-With `@reactive`, you declare the relationships once:
-
-```python
-# Reactive: Declare what should happen
-@reactive(count)
-def update_ui(value):
- print(f"UI: {value}")
-
-@reactive(count)
-def save_to_database(value):
- print(f"Saving: {value}")
-
-@reactive(count)
-def log_change(value):
- print(f"Log: {value}")
-
-# Now just update state
-count.set(42)
-# All three functions run automatically
-# UI: 42
-# Saving: 42
-# Log: 42
-```
-
-You've moved from "remember to update everything" to "declare what should stay synchronized." The burden of coordination shifts from you to FynX.
-
-## Reacting to Multiple Observables
-
-Most real-world reactions depend on multiple pieces of state. `@reactive` accepts multiple observables:
-
-```python
-first_name = observable("Alice")
-last_name = observable("Smith")
-
-@reactive(first_name, last_name)
-def greet(first, last):
- print(f"Hello, {first} {last}!")
-
-# Runs immediately: "Hello, Alice Smith!"
-
-first_name.set("Bob")
-# Runs again: "Hello, Bob Smith!"
-
-last_name.set("Jones")
-# Runs again: "Hello, Bob Jones!"
-```
-
-When you pass multiple observables, the function receives their values as separate arguments, in the same order you listed them. Change any observable, and the function runs with all current values.
-
-This makes coordinating multiple state sources trivial:
-
-```python
-class CartStore(Store):
- items = observable([])
- tax_rate = observable(0.08)
-
-@reactive(CartStore.items, CartStore.tax_rate)
-def update_total_display(items, rate):
- subtotal = sum(item['price'] * item['quantity'] for item in items)
- tax = subtotal * rate
- total = subtotal + tax
- print(f"Total: ${total:.2f}")
-
-# Runs when items change OR when tax_rate changes
-```
-
-You don't write separate subscriptions for each observable. You don't coordinate between them. You just declare: "this function needs these values, run it when any change."
-
-## Reacting to Entire Stores
-
-Sometimes you want to react to *any* change in a Store, regardless of which specific observable changed. Pass the Store class itself:
-
-```python
-class UserStore(Store):
- name = observable("Alice")
- age = observable(30)
- email = observable("alice@example.com")
-
-@reactive(UserStore)
-def sync_to_server(store_snapshot):
- print(f"Syncing: {store_snapshot.name}, {store_snapshot.email}")
-
-# Runs immediately with initial state
-
-# Runs when name changes:
-UserStore.name = "Bob"
-
-# Runs when age changes:
-UserStore.age = 31
-
-# Runs when email changes:
-UserStore.email = "bob@example.com"
-```
-
-The function receives a snapshot of the entire Store. This is perfect for operations that need to consider the complete state—saving to a database, logging changes, synchronizing with a server.
-
-Note the subtle difference: when reacting to individual observables, you get the *values* as arguments. When reacting to a Store, you get the *Store snapshot itself* as a single argument, and you access observables through it.
-
-## Reacting to Computed Observables
-
-Everything that's an observable—including computed ones—works with `@reactive`:
-
-```python
-class CartStore(Store):
- items = observable([])
-
-# Computed observable
-item_count = CartStore.items >> (lambda items: len(items))
-
-@reactive(item_count)
-def update_badge(count):
- print(f"Cart badge: {count}")
-
-# Runs immediately with initial computed value (0)
-
-CartStore.items = [{'name': 'Widget', 'price': 10}]
-# Computed value recalculates: 1
-# Reactive function runs: "Cart badge: 1"
-
-CartStore.items = CartStore.items + [{'name': 'Gadget', 'price': 15}]
-# Computed value recalculates: 2
-# Reactive function runs: "Cart badge: 2"
-```
-
-You don't react to `CartStore.items` directly. You react to the *computed* value. This is powerful: it means you only care about changes in the *derived* state, not every modification to the underlying data.
-
-## Practical Example: Form Validation
-
-Here's where `@reactive` really shines—coordinating complex UI behavior:
-
-```python
-class FormStore(Store):
- email = observable("")
- password = observable("")
- confirm_password = observable("")
-
-# Computed validations
-email_valid = FormStore.email >> (
- lambda e: '@' in e and '.' in e.split('@')[-1]
-)
-
-password_valid = FormStore.password >> (
- lambda p: len(p) >= 8
-)
-
-passwords_match = (FormStore.password | FormStore.confirm_password) >> (
- lambda pwd, confirm: pwd == confirm and pwd != ""
-)
-
-form_valid = (email_valid | password_valid | passwords_match) >> (
- lambda ev, pv, pm: ev and pv and pm
-)
-
-# Reactive UI updates
-@reactive(email_valid)
-def update_email_indicator(is_valid):
- status = "✓" if is_valid else "✗"
- print(f"Email: {status}")
-
-@reactive(password_valid)
-def update_password_indicator(is_valid):
- status = "✓" if is_valid else "✗"
- print(f"Password strength: {status}")
-
-@reactive(passwords_match)
-def update_match_indicator(match):
- status = "✓" if match else "✗"
- print(f"Passwords match: {status}")
-
-@reactive(form_valid)
-def update_submit_button(is_valid):
- state = "enabled" if is_valid else "disabled"
- print(f"Submit button: {state}")
-
-# Reactive functions run immediately with initial validation states
-# Then update the form fields:
-FormStore.email = "alice@example.com"
-# Email: ✓ (email indicator runs)
-
-FormStore.password = "secure123"
-# Password strength: ✓ (password indicator runs)
-# Passwords match: ✗ (match indicator runs - passwords don't match yet)
-
-FormStore.confirm_password = "secure123"
-# Passwords match: ✓ (match indicator runs)
-# Submit button: enabled (form becomes valid)
-```
-
-Every UI element updates automatically in response to the relevant state changes. You never write "when email changes, check if it's valid and update the indicator." You just declare the relationship and FynX handles the orchestration.
-
-## When @reactive Runs: Understanding Execution Order
-
-When multiple observables change in quick succession, reactive functions run in a predictable order:
-
-```python
-count = observable(0)
-
-@reactive(count)
-def first_reaction(value):
- print(f"First: {value}")
-
-@reactive(count)
-def second_reaction(value):
- print(f"Second: {value}")
-
-count.set(5)
-# Output (order may vary):
-# First: 5
-# Second: 5
-```
-
-Reactive functions run in the order they were decorated. This ordering is deterministic but fragile—if reaction order matters to your application, you're probably doing something wrong. Each reaction should be independent, responding only to the observable values it receives.
-
-If you have reactions that depend on each other, consider using computed observables instead:
-
-```python
-# Don't do this: reactions that depend on other reactions
-shared_state = []
-
-@reactive(count)
-def reaction_one(value):
- shared_state.append(value)
-
-@reactive(count)
-def reaction_two(value):
- # This assumes reaction_one has already run
- print(f"Total accumulated: {sum(shared_state)}")
-
-# Do this instead: express dependencies through computed observables
-accumulated = count >> (lambda c: sum(range(c + 1)))
-
-@reactive(accumulated)
-def show_total(total):
- print(f"Total: {total}")
-```
-
-## @reactive vs. Manual Subscriptions
-
-When should you use `@reactive` instead of calling `.subscribe()` directly?
-
-**Use `@reactive` when:**
-
-```python
-# You want declarative, self-documenting code
-@reactive(user_count)
-def update_dashboard(count):
- print(f"Users: {count}")
-
-# You need the function to run immediately with initial state
-@reactive(theme)
-def apply_theme(theme_name):
- load_css(theme_name) # Runs right away
-
-# You're defining reactions at module level or class definition
-class UIController:
- @reactive(AppStore.mode)
- def sync_mode(mode):
- update_ui_mode(mode)
-```
-
-**Use `.subscribe()` when:**
-
-```python
-# You need dynamic subscriptions that change at runtime
-if user_wants_notifications:
- count.subscribe(send_notification)
-
-# You need to unsubscribe conditionally
-subscription_func = count.subscribe(handler)
-if some_condition:
- count.unsubscribe(subscription_func)
-
-# You're building a library that accepts observables
-def create_widget(data_observable):
- data_observable.subscribe(widget.update)
-```
-
-The rule of thumb: `@reactive` for static, declarative reactions that exist for the lifetime of your application. `.subscribe()` for dynamic, programmatic subscriptions that you manage explicitly.
-
-## Common Patterns
-
-**Pattern 1: Syncing to external systems**
-
-```python
-@reactive(AppStore)
-def save_state(store):
- serialized = {
- 'user': store.user,
- 'settings': store.settings
- }
- save_to_local_storage('app_state', serialized)
-```
-
-**Pattern 2: Logging and debugging**
-
-```python
-@reactive(UserStore.login_count)
-def log_logins(count):
- print(f"[DEBUG] Login count: {count}")
- if count > 100:
- print("[WARN] Unusual login activity detected")
-```
-
-**Pattern 3: Cross-store coordination**
-
-```python
-@reactive(ThemeStore.mode)
-def update_editor_theme(mode):
- EditorStore.syntax_theme = "dark" if mode == "dark" else "light"
-```
-
-**Pattern 4: Analytics and tracking**
-
-```python
-@reactive(CartStore.items)
-def track_cart_changes(items):
- analytics.track('cart_updated', {
- 'item_count': len(items),
- 'total_value': sum(item['price'] for item in items)
- })
-```
-
-## Gotchas and Edge Cases
-
-**1. Infinite loops are possible**
-
-```python
-count = observable(0)
-
-@reactive(count)
-def increment_forever(value):
- count.set(value + 1) # DON'T DO THIS
-
-# This will hang your program
-```
-
-FynX doesn't prevent infinite loops. If your reactive function modifies an observable it's reacting to, you create a cycle. The solution: reactive functions should perform *side effects* (UI updates, logging, network calls), not modify the observables they're watching.
-
-**2. Reactive functions don't track .value reads**
-
-```python
-other_count = observable(10)
-
-@reactive(count)
-def show_sum(value):
- print(f"Sum: {value + other_count.value}")
-
-count.set(5) # Prints: "Sum: 15"
-other_count.set(20) # Doesn't trigger show_sum
-```
-
-The function only reacts to observables passed to `@reactive()`. Reading `other_count.value` inside the function doesn't create a dependency. If you want to react to both, pass both:
-
-```python
-@reactive(count, other_count)
-def show_sum(value, other):
- print(f"Sum: {value + other}")
-```
-
-**3. Reactive functions receive values, not observables**
-
-```python
-@reactive(count)
-def try_to_modify(value):
- value.set(100) # ERROR: value is an int, not an observable
-
-# If you need the observable, access it directly:
-@reactive(count)
-def correct_approach(value):
- if value < 0:
- count.set(0) # Access count directly, not through the argument
-```
-
-**4. Store reactions receive snapshots**
-
-```python
-@reactive(UserStore)
-def save_user(store):
- # store is a snapshot of UserStore at this moment
- # store.name is the current value, not an observable
- save_to_db(store.name) # Correct
-
- # This won't give you an observable:
- store.name.subscribe(handler) # ERROR
-```
-
-## Performance Considerations
-
-Reactive functions run synchronously on every change. For expensive operations, consider:
-
-**Debouncing through computed observables:**
-
-```python
-search_query = observable("")
-
-# Computed observable that only changes when meaningful
-filtered_results = search_query >> (
- lambda q: search_database(q) if len(q) >= 3 else []
-)
-
-@reactive(filtered_results)
-def update_ui(results):
- display_results(results) # Only runs when filter criteria met
-```
-
-**Conditional logic inside reactions:**
-
-```python
-@reactive(mouse_position)
-def update_tooltip(position):
- if should_show_tooltip(position): # Guard clause
- expensive_tooltip_render(position)
-```
-
-**Batching updates:**
-
-```python
-pending_saves = []
-
-@reactive(DocumentStore.content)
-def queue_save(content):
- pending_saves.append(content)
- # Actual save happens elsewhere, periodically
-```
-
-## What's Next
-
-`@reactive` gives you automatic reactions to state changes. You can combine it with conditional observables to create event-driven reactions when specific conditions are met. This covers the full spectrum of reactive behaviors—from "keep this in sync" to "do this when that happens."
-
-With observables, stores, and `@reactive`, you have everything you need to build sophisticated reactive applications where state changes automatically propagate through your system, and important transitions trigger the right behaviors at the right times.
-
-## Summary
-
-The `@reactive` decorator transforms functions into automatic reactions that run whenever observables change:
-
-- **Declarative subscriptions** — No manual `.subscribe()` calls to manage
-- **Runs immediately and on changes** — Get initial state and all updates
-- **Works with any observable** — Standalone, Store attributes, computed values, merged observables, conditional observables
-- **Multiple observable support** — React to several sources, receive values as arguments
-- **Store-level reactions** — React to any change in an entire Store
-- **Conditional reactions** — Use with conditional observables for event-driven behavior
-- **Side effects, not state changes** — Reactive functions should perform effects, not modify watched observables
-
-With `@reactive`, you declare *what should happen* when state changes. FynX ensures it happens automatically, in the right order, every time. This eliminates a whole category of synchronization bugs and makes your reactive systems self-maintaining.
diff --git a/docs/generation/mkdocs.yml b/docs/generation/mkdocs.yml
index ddce39c..08f86ee 100644
--- a/docs/generation/mkdocs.yml
+++ b/docs/generation/mkdocs.yml
@@ -1,42 +1,52 @@
site_name: FynX
site_description: "Python Reactive State Management Library"
site_url: https://off-by-some.github.io/fynx/
-docs_dir: ..
+docs_dir: markdown
site_dir: ../../site
# Ignore template files during MkDocs processing
exclude_docs: |
build/templates/**
+extra:
+ version:
+ provider: mike
+ social:
+ - icon: fontawesome/brands/github
+ link: https://github.com/off-by-some/fynx
+ - icon: fontawesome/brands/python
+ link: https://pypi.org/project/fynx/
+
nav:
- - Home: index.md
+ - Introduction:
+ - Home: index.md
+ - Tutorial:
+ - Getting Started: tutorial/observables.md
+ - Derived Observables: tutorial/derived-observables.md
+ - Conditionals: tutorial/conditionals.md
+ - Stores: tutorial/stores.md
+ - Using @reactive: tutorial/using-reactive.md
- API Reference:
- - Overview: generation/markdown/api.md
- - Observables: generation/markdown/observables.md
- - Derived Observables: generation/markdown/derived-observables.md
- - Conditionals: generation/markdown/conditionals.md
- - Stores: generation/markdown/stores.md
- - Using @reactive: generation/markdown/using-reactive.md
- - Reference:
- - Observable Types:
- - Observable: generation/markdown/observable.md
- - ComputedObservable: generation/markdown/computed-observable.md
- - MergedObservable: generation/markdown/merged-observable.md
- - ConditionalObservable: generation/markdown/conditional-observable.md
- - Observable Descriptors: generation/markdown/observable-descriptors.md
- - Observable Operators: generation/markdown/observable-operators.md
- - Reactive Decorators:
- - "@reactive": generation/markdown/reactive-decorator.md
- - "Decorators": generation/markdown/decorators.md
- - "Store & @observable": generation/markdown/store.md
- - Mathematical Foundations: generation/markdown/mathematical-foundations.md
+ - Overview: reference/api.md
+ - Observable Types:
+ - Observable: reference/observable.md
+ - Observable Operators: reference/observable-operators.md
+ - ComputedObservable: reference/computed-observable.md
+ - MergedObservable: reference/merged-observable.md
+ - ConditionalObservable: reference/conditional-observable.md
+ - Observable Descriptors: reference/observable-descriptors.md
+ - Stores: reference/store.md
+ - Reactive Decorators:
+ - "@reactive": reference/reactive-decorator.md
+ - Mathematical Foundations:
+ - Mathematical Foundations: mathematical/mathematical-foundations.md
- Roadmap:
- - v1.0 Roadmap: specs/v1.0-roadmap.md
+ - v1.0 Roadmap: ../specs/v1.0-roadmap.md
theme:
name: material
- favicon: images/icon_350x350.png
- logo: images/icon_350x350.png
+ favicon: assets/images/icon_350x350.png
+ logo: assets/images/icon_350x350.png
logo_text: FynX
logo_text_align: center
palette:
@@ -108,20 +118,11 @@ markdown_extensions:
- toc:
permalink: true
-extra:
- version:
- provider: mike
- social:
- - icon: fontawesome/brands/github
- link: https://github.com/off-by-some/fynx
- - icon: fontawesome/brands/python
- link: https://pypi.org/project/fynx/
-
extra_css:
- - ../stylesheets/extra.css
+ - assets/stylesheets/extra.css
extra_javascript:
- - javascripts/mathjax.js
+ - assets/javascripts/mathjax.js
- https://polyfill.io/v3/polyfill.min.js?features=es6
- https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js
diff --git a/docs/generation/scripts/generate_html.py b/docs/generation/scripts/generate_html.py
old mode 100755
new mode 100644
index 3f8dc0e..16d3ac2
--- a/docs/generation/scripts/generate_html.py
+++ b/docs/generation/scripts/generate_html.py
@@ -27,11 +27,11 @@ def generate_html_docs() -> None:
sys.exit(1)
# Ensure docs directory and index.md exist
- docs_dir = Path("docs")
- index_md = docs_dir / "index.md"
+ markdown_dir = Path(__file__).parent.parent / "markdown"
+ index_md = markdown_dir / "index.md"
if not index_md.exists():
print(f"❌ Index file not found at {index_md}")
- print(" Please ensure docs/index.md exists")
+ print(" Please ensure docs/generation/markdown/index.md exists")
sys.exit(1)
# Build the HTML documentation
@@ -43,11 +43,14 @@ def generate_html_docs() -> None:
project_root = Path(__file__).parent.parent.parent.parent
env["PYTHONPATH"] = str(project_root)
+ # Change to the generation directory where mkdocs.yml is located
+ generation_dir = Path(__file__).parent.parent
+
result = subprocess.run(
- ["poetry", "run", "mkdocs", "build", "-f", str(mkdocs_config_path)],
+ ["poetry", "run", "mkdocs", "build"],
capture_output=True,
text=True,
- cwd=".",
+ cwd=str(generation_dir),
env=env,
)
diff --git a/docs/generation/scripts/preview_html_docs.sh b/docs/generation/scripts/preview_html_docs.sh
index 8bc9944..500b80a 100755
--- a/docs/generation/scripts/preview_html_docs.sh
+++ b/docs/generation/scripts/preview_html_docs.sh
@@ -27,5 +27,5 @@ echo " Press Ctrl+C to stop the server"
echo ""
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
-MKDOCS_CONFIG="$SCRIPT_DIR/../mkdocs.yml"
-poetry run mkdocs serve -f "$MKDOCS_CONFIG"
+cd "$SCRIPT_DIR/.."
+poetry run mkdocs serve --dev-addr=127.0.0.1:8000
diff --git a/docs/specs/v1.0-roadmap.md b/docs/specs/v1.0-roadmap.md
index 18bbb83..f74a993 100644
--- a/docs/specs/v1.0-roadmap.md
+++ b/docs/specs/v1.0-roadmap.md
@@ -582,7 +582,7 @@ subscription.unsubscribe() # Release callback reference
**Acceptance Criteria**:
☐ `&` operator: `obs1 & obs2` returns observable evaluating AND during propagation
-☐ `|` operator: `obs1 | obs2` returns observable evaluating OR during propagation (distinct from merge `@`)
+☐ `+` operator: `obs1 + obs2` returns observable evaluating OR during propagation (distinct from merge `@`)
☐ `~` operator: `~obs` returns observable negating value during propagation
☐ Operators work element-wise on current values at propagation time
☐ For finite sources: produces `FiniteObservable