ND-Recursion
Sommaire
- ~~ ND-Recursion
- Forme combinateur
- Factorielle
- Forme déclaration
- ~> ND-Map
- Forme lift
- Forme applicative
- Broadcast ND-map
- ~[] Empty Topos
- Broadcast ND
- data.[~> f] Broadcast ND-map
- data.[~~ lambda] Broadcast ND-recursion
- Chaînage
- Mode d'exécution
- Pragmas ND
- Memoization
- Batching (Phase 6)
- Limites de récursion
Exemples d'utilisation de la ND-récursion (~~, ~>, ~[]).
La ND-récursion permet d'exprimer des computations récursives qui peuvent être exécutées en parallèle sans syntaxe
async/await. Le runtime choisit le mode d'exécution.
~~ ND-Recursion
Forme combinateur
La forme combinateur applique la lambda avec une seed initiale:
# Countdown récursif
~~(5, (v, recur) => {
if v > 0 {
recur(v - 1)
} else {
v
}
})
# → 0
Explication: La lambda reçoit (v, recur) où:
v= valeur couranterecur= fonction pour continuer la récursion
Factorielle
~~(5, (n, recur) => {
if n <= 1 { 1 }
else { n * recur(n - 1) }
})
# → 120
Forme déclaration
Créer une fonction ND-récursive réutilisable:
countdown = ~~(n, recur) => {
if n > 0 { recur(n - 1) }
else { "done" }
}
countdown(10)
# → "done"
À ce stade, la lambda wrappée s'exécute comme une fonction normale. Une fois appelée avec une seed, elle déclenche la récursion.
~> ND-Map
Forme lift
Lifter une fonction dans le contexte ND:
f = ~> abs
f(-5)
# → 5
Forme applicative
Appliquer une fonction en contexte ND:
~>(list(-1, -2, -3), abs)
# → [1, 2, 3]
Broadcast ND-map
Mapper une fonction sur chaque élément:
list(-1, -2, 3).[~> abs]
# → [1, 2, 3]
Avec lambda:
list(1, 2, 3).[~> (x) => { x * 2 }]
# → [2, 4, 6]
~[] Empty Topos
Littéral représentant le topos vide (élément neutre):
empty = ~[]
Propriétés:
# Falsy en contexte booléen
if ~[] { 1 } else { 2 }
# → 2
# Égalité
~[] == ~[]
# → True
# Longueur
len(~[])
# → 0
Le topos vide sert d'élément identité pour les opérations ND. Il marque la terminaison dans les graphes de calcul.
Broadcast ND
Les opérateurs ND fonctionnent avec le broadcast sur collections.
data.[~> f] Broadcast ND-map
Applique une fonction à chaque élément en contexte ND:
# Map abs sur liste
list(-1, -2, 3).[~> abs]
# → [1, 2, 3]
# Map lambda inline
list(1, 2, 3).[~> (x) => { x * 2 }]
# → [2, 4, 6]
# Préserve le type tuple
tuple(-1, -2, 3).[~> abs]
# → (1, 2, 3)
data.[~~ lambda] Broadcast ND-recursion
Applique ND-recursion à chaque élément:
# Factorielle sur liste
list(3, 5).[~~(n, recur) => {
if n <= 1 { 1 }
else { n * recur(n - 1) }
}]
# → [6, 120]
# Countdown sur chaque élément
list(3, 5, 2).[~~(v, recur) => {
if v > 0 { recur(v - 1) }
else { v }
}]
# → [0, 0, 0]
# Préserve le type tuple
tuple(3, 2, 4).[~~(n, recur) => {
if n <= 1 { 1 }
else { n * recur(n - 1) }
}]
# → (6, 2, 24)
Note: Le broadcast ND préserve automatiquement le type de la collection (list, tuple, set).
Chaînage
Les opérateurs ND se chaînent naturellement avec le broadcasting standard:
data = list(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
result = data.[if > 3].[* 2].[if < 15].[+ 1]
# > 3: [4,5,6,7,8,9,10]
# * 2: [8,10,12,14,16,18,20]
# < 15: [8,10,12,14]
# + 1: [9,11,13,15]
Mode d'exécution
Par défaut, exécution séquentielle. Le mode "thread" utilise rayon (Rust) pour distribuer les éléments sur un pool
de threads natifs, en relâchant le GIL pendant l'exécution.
Pragmas ND
Les pragmas permettent de contrôler le mode d'exécution :
# Mode parallèle (rayon)
pragma("nd_mode", ND.thread)
~~(huge_dataset, (data, recur) => { … })
Modes disponibles :
"sequential"(défaut) : exécution séquentielle, pas de overhead"thread": distribution rayon avecpy.detach()/Python::attach()par thread
Le parallélisme repose sur rayon (Rust), pas sur des threads Python. Le GIL est relâché pendant le dispatch et réacquis par chaque worker pour les callbacks Python. La memoization reste thread-safe via
Arc<Mutex>.
Memoization
Le pragma nd_memoize active un cache automatique des résultats :
# Activer la memoization
pragma("nd_memoize", True)
# Fibonacci avec memoization - 11x speedup sur fib(25)
~~(25, (n, recur) => {
if n <= 1 { n }
else { recur(n - 1) + recur(n - 2) }
})
# → 75025
# Désactiver la memoization (défaut)
pragma("nd_memoize", False)
Principe : Le scheduler cache les résultats par valeur seed. Si recur(n) est rappelé avec la même valeur n, le
résultat en cache est retourné au lieu de recalculer.
Cas d'usage optimal :
- Algorithmes avec redondance (Fibonacci, DP)
- Calculs coûteux sur mêmes valeurs
- Broadcast sur collections avec valeurs dupliquées
Performance :
- Fibonacci sans memoization : O(2^n) appels
- Fibonacci avec memoization : O(n) appels
- Speedup mesuré : 11x sur fib(25), plus élevé pour n plus grand
Batching (Phase 6)
Le pragma nd_batch_size contrôle la granularité du parallélisme en regroupant plusieurs items avant de les soumettre à
ThreadPoolExecutor :
# Configuration explicite
pragma("nd_mode", ND.process)
pragma("nd_workers", 8)
pragma("nd_batch_size", 10) # 10 items par batch
# Large collection - batching réduit l'overhead
range(1, 101).[~~(n, recur) => {
if n <= 1 { 1 }
else { n * recur(n - 1) }
}]
# Auto-calcul (défaut avec 0)
pragma("nd_batch_size", 0) # batch_size = ceil(len / (workers * 4))
Principe : Au lieu de soumettre chaque item individuellement à l'executor, on groupe batch_size items ensemble.
Cela réduit le nombre de submits et l'overhead de synchronisation.
Auto-calcul : Si batch_size = 0 (défaut), le scheduler calcule automatiquement pour obtenir ~4 batches par worker.
Détection intelligente : Pour les petites collections (< workers*2 items), le batching est automatiquement désactivé pour éviter l'overhead.
Cas d'usage optimal :
- Collections grandes (100+ items)
- Mode parallel avec plusieurs workers
- Items avec temps de calcul variable
Combinaison avec memoization :
pragma("nd_mode", ND.process)
pragma("nd_memoize", True)
pragma("nd_batch_size", 5)
# Broadcast sur collection avec doublons
# Batching: réduit overhead ThreadPoolExecutor
# Memoization: évite recalculs des valeurs déjà vues
list(10, 12, 10, 15, 12, 20).[~~(n, recur) => {
if n <= 1 { n }
else { recur(n - 1) + recur(n - 2) }
}]
Limites de récursion
La récursion ND est limitée à 200 appels imbriqués via recur(). Au-delà, une RecursionError est levée :
# Récursion infinie : déclenche RecursionError
~~(0, (v, recur) => { recur(v + 1) })
# RecursionError: maximum ND recursion depth exceeded
Cette limite protège contre les stack overflows Rust. Chaque appel récursif crée une nouvelle instance VM sur la stack (~16KB par frame), et la stack thread (~8MB) déborde autour de ~494 niveaux.
Pour les cas légitimes nécessitant plus de profondeur, préférer la récursion classique (fonctions =>) qui utilise le
frame stacking de la VM sans limite de stack Rust.
Principe : La sémantique du code reste identique, seul le mode d'exécution change. Le déterminisme est préservé - résultat identique en séquentiel et parallèle.
Les optimisations (memoization, batching) réduisent le temps d'exécution sans changer le résultat.