A macro-based DSL for creating Elixir structs with automatic dependency-based field recomputation. When dependencies change, computed fields are automatically updated in topological order.
- Automatic dependency tracking: Fields recompute only when their dependencies change
- Topological sorting: Computations execute in correct order for dependency chains
- Lazy evaluation: Only affected fields are recomputed
- Clean API: Simple
new/1
,update/2
, andput/3
functions - Visualization: Generate Mermaid flowcharts of field dependencies
Add reactive_struct
to your list of dependencies in mix.exs
:
def deps do
[
{:reactive_struct, github: "chgeuer/reactive_struct"}
]
end
use ReactiveStruct
in your module (optionally with configuration)- Define your struct with
defstruct
- Define computed fields with
computed/2
macro - Create instances with
new/1
and modify withupdate/2
ReactiveStruct supports the following options when using the macro:
allow_updating_computed_fields
(default:false
) - Controls whether computed fields can be directly updated
# Default behavior - computed fields cannot be updated directly
use ReactiveStruct
# Allow updating computed fields (breaks dependency graph)
use ReactiveStruct, allow_updating_computed_fields: true
defmodule Calculator do
use ReactiveStruct
defstruct ~w(a0 a b sum product)a
computed(:a, fn %{a0: a0} -> a0 + 1 end)
computed(:sum, fn %{a: a, b: b} -> a + b end)
computed :product, fn %{a: a, b: b} ->
a * b
end
end
Calculator.new(a0: 2, b: 3)
|> IO.inspect(label: :a) # %Calculator{a0: 2, a: 3, b: 3, sum: 6, product: 9}
|> Calculator.merge(a0: 5)
|> IO.inspect(label: :b) # %Calculator{a0: 5, a: 6, b: 3, sum: 9, product: 18}
Calling Kino.Mermaid.new(Calculator.mermaid())
results in this little diagram (in Elixir LiveBook):
flowchart TD
%% Input fields
A0[a0]
B[b]
%% Computed fields
A[a]
PRODUCT[product]
SUM[sum]
%% Dependencies
A --> PRODUCT
A --> SUM
A0 --> A
B --> PRODUCT
B --> SUM
%% Styling
classDef inputField fill:#e1f5fe,stroke:#0277bd,stroke-width:2px
classDef computedField fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px
class B,A0 inputField
class SUM,PRODUCT,A computedField
computed(:field_name, fn %{dep1: dep1, dep2: dep2} ->
dep1 + dep2
end)
ReactiveStruct handles chains of dependencies automatically:
defmodule Chain do
use ReactiveStruct
defstruct [:base, :step1, :step2, :final]
computed :step1, fn %{base: base} -> base * 2 end
computed :step2, fn %{step1: step1} -> step1 + 10 end
computed :final, fn %{step2: step2} -> step2 * step2 end
end
chain = Chain.new(base: 3)
chain.final # 256
updated = Chain.merge(chain, :base, 5)
updated.final # 400
By default, ReactiveStruct prevents direct updates to computed fields to maintain dependency graph integrity:
defmodule Calculator do
use ReactiveStruct
defstruct [:a, :b, :sum]
computed :sum, fn %{a: a, b: b} -> a + b end
end
calc = Calculator.new(a: 1, b: 2)
# ✓ This works - updating input fields
Calculator.merge(calc, :a, 10)
# ✗ This raises ArgumentError - updating computed field
Calculator.merge(calc, :sum, 100)
To allow updating computed fields (which may break dependency consistency):
defmodule FlexibleCalculator do
use ReactiveStruct, allow_updating_computed_fields: true
defstruct [:a, :b, :sum]
computed :sum, fn %{a: a, b: b} -> a + b end
end
calc = FlexibleCalculator.new(a: 1, b: 2)
# ✓ This now works - computed field can be updated directly
FlexibleCalculator.merge(calc, :sum, 100)
ReactiveStruct generates these functions:
new/1
- Create instance with initial values (map or keyword list)merge/2
- Update multiple fields (map or keyword list)merge/3
- Update single fieldmermaid/0
- Generate dependency visualization in MermaidJS syntax
# Multiple field updates
multi = MyStruct.merge(struct, %{x: 10, y: 20})
multi = MyStruct.merge(struct, x: 10, y: 20)
# Single field update
single = MyStruct.merge(struct, :x, 100)
Generate Mermaid flowcharts to visualize field dependencies:
defmodule Example do
use ReactiveStruct
defstruct [:a, :b, :sum, :product]
computed :sum, fn %{a: a, b: b} -> a + b end
computed :product, fn %{a: a, b: b} -> a * b end
end
Example.mermaid()
|> Kino.Mermaid.new()
results in
flowchart TD
%% Input fields
A[a]
B[b]
%% Computed fields
PRODUCT[product]
SUM[sum]
%% Dependencies
A --> PRODUCT
A --> SUM
B --> PRODUCT
B --> SUM
%% Styling
classDef inputField fill:#e1f5fe,stroke:#0277bd,stroke-width:2px
classDef computedField fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px
class B,A inputField
class SUM,PRODUCT computedField
ReactiveStruct validates dependencies at compile time and runtime:
- Circular dependencies: Compilation error
- Non-existent fields: Compilation error
- Computed field updates: Runtime ArgumentError (unless explicitly allowed)
- Runtime exceptions: Propagated from computation functions
- Nil dependencies: Handle in computation logic as needed
- Compilation: O(n²) for dependency analysis
- Runtime updates: O(k) where k = affected computed fields
- Memory: Minimal overhead, metadata stored at compile time