03.04 — reduce / fold dans Ocarina#

Un seul reduce dans toute la base de code. Mais c’est le réducteur central : il fait fonctionner chain_actions.

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)

Anatomie#

Concept FPRéalisation Python
Type accumulatorActionChain[T]
Type elementActionSuccess[T]
Reducerreducer(chain, step) -> ActionChain[T]
Initial valuefirst.execute()
Iterablerest (le tuple de *rest)
Short-circuitif chain.has_failed(): return chain
ResultActionChain[T] (le dernier)

Flot d’exécution#

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 lève → 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 du reduce = <failed ActionChain>

Pourquoi reduce#

# Approche impérative (équivalente)
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
ReduceBoucle impérative
Une seule valeur circule (le chain)Un état + une mutation
Reducer = fonction pure (testable en isolation)Logique inline
Sémantique « accumulateur » expliciteLogique implicite
Standard fonctionnelStandard impératif

C’est une expression d’intention : on replie une liste sur un accumulator. La sémantique est claire dès la première lecture. C’est ce qu’on appelle un catamorphisme.

break vs return chain dans le reducer#

Le reducer ne peut pas break : il doit retourner une valeur. Donc il retourne chain inchangé à chaque step après l’échec.

C’est exactement ce qu’on veut : le fold continue sur les éléments restants, mais chaque appel du reducer renvoie le même chain failed : c’est un court-circuit.

anyall = folds Pythoniques#

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(...) est un fold OR avec court-circuit. C’est trois for imbriqués qu’on plie en un seul bool.

C’est la même mécanique conceptuelle que chain_actions.reducer, juste sans avoir besoin de reduce explicite.

Pourquoi pas toolz, funcy, ou un wrapper maison#

Le projet n’importe aucune lib FP tierce. functools.reduce est dans la stdlib depuis 30 ans. Pas de magie, pas de risque de breaking change, pas de dette d’audit.