# 9. Reuse: parametrized fragments The retry idea from [step 5](05-selectors) is generic: *attempt something, check the result, loop on failure*. You don't want to hand-wire that into every state that needs it. A **fragment** is a parametrized piece of machine you define once and splice in wherever you need it, filling its blanks at the use site. ## Define once, parametrize the blanks ```text fragment AttemptWithRetry(work: action, check: action) { initial Attempt state Attempt { on enter work } state Waiting {} state Done {} from Attempt select check on Result { "ok" to Done "fail" to Waiting } from Waiting to Attempt } ``` The parameters in `(…)` are the customizable surface. Each has a **kind**: | Kind | Filled with | Used for | | ---- | ----------- | -------- | | `action` | a handler/literal | a hook or selector inside the fragment | | `guard` | a predicate | a `where` condition | | `state` | a state name | a transition target (jump out to the consumer's states) | | `value` | a literal | a `timeout`, or an action input | | `event` | an event name | the trigger of an `on ` transition | Here `work` and `check` are `action` parameters: the consumer supplies *what to attempt* and *how to judge the result*, and the loop structure is reused as-is. ## Use it, filling the parameters `use (args) as ` splices the fragment in as a child composite named ``: ```python from harel import definition_from_dsl, DurableRunner, DictStore, Event SOURCE = """ event Result {} fragment AttemptWithRetry(work: action, check: action) { initial Attempt state Attempt { on enter work } state Waiting {} state Done {} from Attempt select check on Result { "ok" to Done "fail" to Waiting } from Waiting to Attempt } machine order { initial Charging use AttemptWithRetry(work=charge, check=charge_result) as Charging final Paid success {} from Charging to Paid } """ attempts = {"n": 0} def charge(stm, event, **inputs): stm.execution_ctx.setdefault("charges", []).append("charge") def charge_result(stm, event, **inputs): attempts["n"] += 1 return "ok" if attempts["n"] >= 3 else "fail" # succeed on the third try defn = definition_from_dsl(SOURCE, "order", actions={"charge": charge, "charge_result": charge_result}) runner = DurableRunner(DictStore(), {defn.id: defn}) exe = runner.create(defn.id) print("start ->", exe.active_path) for i in (1, 2, 3): exe = runner.process(exe.id, Event(kind="Result")) print(f"Result #{i} ->", exe.active_path) print("final:", exe.active_path, "/", exe.outcome, "| charge attempts:", len(exe.context["charges"])) ``` ```text start -> Charging.Attempt Result #1 -> Charging.Attempt Result #2 -> Charging.Attempt Result #3 -> Paid final: Paid / success | charge attempts: 3 ``` Each failing `Result` routes to `Waiting`, which bounces straight back to `Attempt` — re-running `charge`. On the third attempt `charge_result` returns `"ok"`, the fragment's `Done` sink completes the `Charging` composite, and the consumer's `from Charging to Paid` fires. Because the loop is parametrized, the *same* fragment can wrap any retryable step — charging a card, calling a carrier, reserving stock — by using it again with different `work`/`check` arguments. The repository's `retry.stm` is the full version of this pattern, adding a `value` parameter for the backoff policy and a budget on the consumer. ## Fragments compose: a fragment that uses a fragment A fragment body may itself `use` another fragment — and **forward its own parameters** as the nested use's arguments. All five kinds forward (action, guard, value, state, event), resolved against the enclosing fragment's scope, so you can build a higher-level fragment on top of a lower-level one and thread the blanks straight through. Here `RetryStep` wraps `AttemptWithRetry`, forwarding its `task`/`verdict` actions and its `attempts` **value** down into the inner fragment (where `attempts` lands as the `budget` input of `work`). The machine fills `RetryStep` once; the value travels two levels deep: ```python from harel import definition_from_dsl, DurableRunner, DictStore, Event SOURCE = """ event Result {} fragment AttemptWithRetry(work: action, check: action, budget: value) { initial Attempt state Attempt { on enter work(budget: budget) } # the forwarded value reaches the action's inputs state Waiting {} state Done {} from Attempt select check on Result { "ok" to Done "fail" to Waiting } from Waiting to Attempt } fragment RetryStep(task: action, verdict: action, attempts: value) { initial Begin state Begin {} use AttemptWithRetry(work = task, check = verdict, budget = attempts) as Try # forward all three from Begin to Try } machine order { initial Charging use RetryStep(task = charge, verdict = charge_result, attempts = 3) as Charging final Paid success {} from Charging to Paid } """ seen = {} def charge(stm, event, **inputs): seen["budget"] = inputs.get("budget") # the value forwarded two levels down stm.execution_ctx["tries"] = stm.execution_ctx.get("tries", 0) + 1 def charge_result(stm, event, **inputs): return "ok" if stm.execution_ctx["tries"] >= seen["budget"] else "fail" defn = definition_from_dsl(SOURCE, "order", actions={"charge": charge, "charge_result": charge_result}) runner = DurableRunner(DictStore(), {defn.id: defn}) exe = runner.create(defn.id) print("start ->", exe.active_path, "| budget seen by charge:", seen["budget"]) for i in (1, 2, 3): exe = runner.process(exe.id, Event(kind="Result")) print(f"Result #{i} ->", exe.active_path) print("final:", exe.active_path, "/", exe.outcome) ``` ```text start -> Charging.Try.Attempt | budget seen by charge: 3 Result #1 -> Charging.Try.Attempt Result #2 -> Charging.Try.Attempt Result #3 -> Paid final: Paid / success ``` The inner fragment is spliced two composites deep (`Charging.Try.Attempt`), and `attempts = 3` forwarded through `RetryStep` into `AttemptWithRetry`'s `budget` — so `charge` is called with `budget=3` and the loop succeeds on the third try. A name forwarded that the enclosing fragment doesn't declare is a `DslError`, so a typo'd forward fails loudly rather than leaking the name. So far everything has lived in one file. Real projects split machines across files — [imports](10-imports) are next.