03.04 — reduce / fold in Ocarina#

One reduce in the whole code base. But it’s the central reducer — it’s what makes chain_actions work.

Code#

# src/ocarina/dsl/testing_with_railway/chain_actions.py
from functools import reduce

def chain_actions[T](
    first: ActionSuccess[T], *rest: ActionSuccess[T]
) -> ChainRunner[T]:

    def thunk() -> ActionChain[T]:
        def reducer(chain: ActionChain[T], step: ActionSuccess[T]) -> ActionChain[T]:
            if chain.has_failed():
                return chain
            return (
                chain.then(step.__action__)
                .failure(step.__failure_handler__)
                .success(step.__success_handler__)
                .execute()
            )
        return reduce(reducer, rest, first.execute())

    return ChainRunner(thunk=thunk)

Anatomy#

FP conceptPython realization
Accumulator typeActionChain[T]
Element typeActionSuccess[T]
Reducerreducer(chain, step) -> ActionChain[T]
Initial valuefirst.execute()
Iterablerest (the *rest tuple)
Short-circuitif chain.has_failed(): return chain
ResultActionChain[T] (the last one)

Execution flow#

initial = first.execute()                                          # → ActionChain(ok=True, result=Ok(...))

step 1 : reducer(<initial>, act2) :
  chain.has_failed() = False
  chain.then(act2.__action__).failure(...).success(...).execute()
  → action raises → Fail
  → failure_handler(exc)  ❌ FIRE
  → ActionChain(ok=False, result=Fail(exc))

step 2 : reducer(<failed>, act3) :
  chain.has_failed() = True
  return chain                                                     ⚠️  short-circuit

result of the reduce = <failed ActionChain>

Why reduce#

# Imperative approach (equivalent)
def thunk() -> ActionChain[T]:
    chain = first.execute()
    for step in rest:
        if chain.has_failed():
            break
        chain = (
            chain.then(step.__action__)
            .failure(step.__failure_handler__)
            .success(step.__success_handler__)
            .execute()
        )
    return chain
ReduceImperative loop
A single value flows (the chain)A state + a mutation
Reducer = pure function (testable in isolation)Inline logic
Explicit “accumulator” semanticsImplicit logic
Functional standardImperative standard

An expression of intent: we fold a list onto an accumulator. Semantics jump out on the first read. This is what’s called a catamorphism.

break vs return chain in the reducer#

The reducer can’t break — it must return a value. So it returns chain unchanged at every step after the failure.

Exactly what we want: the fold rolls over the remaining elements, each reducer call returns the same failed chain. Short-circuit.

any / all = Pythonic folds#

def has_test_cycle_failed(results: TestCycleResults) -> bool:
    return any(
        is_test_result_fail(outcome)
        for campaigns in results.values()
        for tests in campaigns.values()
        for outcome, _, _ in tests.values()
    )

any(...) is an OR-fold with short-circuit. Three nested fors folded into a single bool.

Same conceptual mechanic as chain_actions.reducer, just without the explicit reduce.

Why not toolz, funcy, or a homemade wrapper#

The project imports zero third-party FP libs. functools.reduce has been in the stdlib for 30 years. No magic, no breaking-change risk, no audit debt.