Getting started
Hello, Causaloid
This page walks through the smallest program that exercises the library: a Causaloid that wraps a pure predicate, then a second Causaloid composed with the first, then evaluation against a value.
The point is not to be impressive. The point is to leave you with a runnable seed you can mutate.
What a Causaloid is, in one paragraph
A Causaloid is a self-contained unit of causality. It wraps a function that takes some input, optionally consults a context, and returns a PropagatingEffect. Causaloids compose: a graph of Causaloids is itself a Causaloid. That recursive shape is what lets the library handle both a single rule and a thousand-rule system with one type.
The full concept page is here. For now you only need the shape.
A first Causaloid
use deep_causality::prelude::*;
fn main() -> Result<(), CausalityError> {
// A Causaloid wrapping a stateless predicate on f64.
let above_zero: BaseCausaloid<f64> = Causaloid::from_causal_fn(
1,
"above_zero",
"value is greater than zero",
|x: &f64| -> Result<PropagatingEffect, CausalityError> {
Ok(PropagatingEffect::Deterministic(*x > 0.0))
},
);
let effect = above_zero.evaluate(&3.5)?;
println!("effect = {:?}", effect);
Ok(())
}
Three things to notice.
The Causaloid carries an id, a short name, and a description. They are not decoration. They show up in the EffectLog when a chain of Causaloids fires, which is what makes the chain auditable later.
The wrapped function returns Result<PropagatingEffect, CausalityError>. PropagatingEffect is a disjoint union of effect shapes; here we use Deterministic(bool) because the predicate yields a boolean. The full set is described in the Effect Propagation Process page.
evaluate runs the Causaloid on a single input. For richer composition you reach for CausaloidGraph or one of the collection forms.
Compose two Causaloids
A trading-style example: a fast/slow moving-average cross plus a volume confirmation. Each rule is its own Causaloid. The conjunction is a graph node whose evaluation depends on both.
use deep_causality::prelude::*;
#[derive(Default, Clone, Debug)]
struct Tick {
fast_ma: f64,
slow_ma: f64,
volume: f64,
median_volume: f64,
}
fn main() -> Result<(), CausalityError> {
let cross = Causaloid::from_causal_fn(
1,
"cross",
"fast MA above slow MA",
|t: &Tick| Ok(PropagatingEffect::Deterministic(t.fast_ma > t.slow_ma)),
);
let confirm = Causaloid::from_causal_fn(
2,
"confirm",
"volume above 1.4x median",
|t: &Tick| Ok(PropagatingEffect::Deterministic(t.volume > t.median_volume * 1.4)),
);
let mut g = CausaloidGraph::new();
let a = g.add_root_causaloid(cross);
let b = g.add_causaloid(confirm);
g.add_edge(a, b)?;
let signal: BaseCausaloid<Tick> = Causaloid::from_causal_graph(
3, "signal", "stage-gated trading signal", g,
);
let tick = Tick { fast_ma: 102.5, slow_ma: 100.0, volume: 2_400.0, median_volume: 1_500.0 };
let effect = signal.evaluate(&tick)?;
println!("{:?}", effect);
Ok(())
}
The graph has two nodes and one edge. The composed Causaloid evaluates both nodes against the same input and reduces the per-node PropagatingEffect values into a single result. The reduction strategy is encoded in the graph; the default is conjunction.
Reading the effect
A PropagatingEffect::Deterministic(true) is the unambiguous case. Other variants carry richer payloads:
Numerical(f64)for a continuous decision.Probabilistic(f64)when the rule produces a probability.Map,RelayTo, andContextualLinkfor cross-references that get resolved later.
Pattern-match on the variant you expect, and let the others surface an error if the upstream rule is misbehaving.
Where to go next
The single-input shape is enough to model many real workflows. When the rules need to see an environment beyond the input value, you move from Causaloid to ContextualCausalFn and add a Context. The next page wires that up.