Notation de Broadcasting `target.[op operand]`

Le broadcasting permet d'appliquer des opérations scalaires à des collections (listes, tuples) sans branches conditionnelles. Trois modes sont disponibles : map (transformation), filter (filtrage conditionnel), et masques booléens (indexation).

data = list(3, 8, 2, 9, 5)

data.[* 2]       # Map: [6, 16, 4, 18, 10]
data.[> 5]       # Map: [False, True, False, True, False]
data.[if > 5]    # Filter: [8, 9]
data.[list(True, False, True, False, False)]  # Masque: [3, 2]

Objectif

Définir une notation permettant d'appliquer des opérations scalaires à des objets de dimension indéterminée (scalaires, vecteurs, matrices, DataFrames) sans branchements conditionnels.

Le broadcasting permet d’écrire une seule expression qui fonctionne aussi bien sur un scalaire que sur une collection (liste, tuple, ou tout iterable), sans branches conditionnelles. Pour les objets multi-dimensionnels (arrays, DataFrames…), la dimension exacte est gérée soit par la bibliothèque sous-jacente, soit par la fonction appliquée. Catnip ne fait pas encore de ND-broadcast récursif interne.

En pratique, cela remplace plusieurs blocs if isinstance(…) par une seule expression A.[op M], ce qui réduit le volume de code et le nombre de chemins d’exécution à maintenir.

Le Concept

Notation et Opérations

Le broadcasting applique des opérations sur des collections (listes, tuples, etc.) de manière uniforme, sans branches conditionnelles.

Trois modes d'opération :

Syntaxe Mode Résultat Exemple
data.[op value] Map Collection de même taille avec transformation list(1,2,3).[* 2][2,4,6]
data.[if op value] Filter Collection filtrée (taille ≤ originale) list(1,2,3).[if > 1][2,3]
data.[mask] Masque booléen Collection filtrée par masque list(1,2,3).[list(True,False,True)][1,3]

Principe

Le broadcasting permet d'écrire des opérations qui portent indifféremment sur des scalaires ou des collections, sans embranchements logiques (if/elif/else).

Une seule expression fonctionne que les données soient :

  • Un scalaire (int, float, str, bool)
  • Une liste Python
  • Un tuple
  • Tout autre itérable

Motivation

Problème Actuel (Python/Pandas)

Quand on travaille avec des DataFrames pandas, le résultat d'une opération peut être :

  • Un scalaire
  • Une Series
  • Une DataFrame

Cela force à écrire du code avec branches :

# Code verbeux et répétitif
result = df.query("age > 30")["salary"]

if isinstance(result, pd.Series):
    doubled = result * 2
elif isinstance(result, pd.DataFrame):
    doubled = result * 2
elif isinstance(result, (int, float)):
    doubled = result * 2
else:
    raise TypeError("Type non supporté")

Solution avec M.[A]

# Code linéaire, sans branches
result = df.query("age > 30")["salary"]
doubled = 2.[result]  # Fonctionne peu importe le type

Syntaxe Implémentée : A.[op M]

La syntaxe Option 2 : Opération à Droite a été choisie et implémentée.

Syntaxe

target.[operator operand]  # Opération binaire (map)
target.[operator]           # Opération unaire (map)
target.[lambda]             # Lambda/fonction (map)
target.[if operator operand]  # Filtrage conditionnel
target.[if lambda]          # Filtrage par lambda
target.[boolean_mask]       # Indexation par masque booléen

Exemples Fonctionnels

# Multiplication
doubled = data.[* 2]

# Addition
shifted = values.[+ 10]

# Puissance
squares = data.[** 2]

# Comparaison (retourne booléens)
masks = data.[> 3]

# Filtrage conditionnel (retourne éléments)
filtered = data.[if > 3]

# Lambda
tripled = data.[(x) => { x * 3 }]

Avantages de Cette Syntaxe

  • Chaînage explicite : data.[* 2].[+ 10]
  • Style orienté données : Lecture gauche à droite
  • Familier : Rappelle .apply() de pandas
  • Lisible : L'objet traité vient en premier

Broadcast sur scalaires littéraux

La notation de broadcast .[…] s’applique aussi aux scalaires littéraux. Un littéral (ex: 5, 3.14, "foo") est un primary et peut donc chaîner des membres, y compris un broadcast.

5.[+ 10]        # → 15
(2 + 3).[* 2]   # → 10
5.[abs]         # → 5

# Filtrage sur un scalaire : retourne une liste
5.[if > 0]      # → list(5)
5.[if < 0]      # → list()  (liste vide)

Map, Filter et Masques Booléens

Distinction Map vs Filter

Le broadcasting supporte deux modes distincts :

Map (transformation) : Transforme chaque élément et retourne une collection de même taille

data = list(3, 8, 2, 9, 5)
masks = data.[> 5]      # [False, True, False, True, False]

Filter (filtrage) : Ne garde que les éléments qui satisfont une condition

data = list(3, 8, 2, 9, 5)
filtered = data.[if > 5]  # [8, 9]

La différence est critique pour le chaînage d'opérations :

# PIÈGE : map puis multiply donne 0 et 2
data.[> 5].[* 2]       # [0, 2, 0, 2, 0]  (False*2=0, True*2=2)

# CORRECT : filter puis multiply
data.[if > 5].[* 2]    # [16, 18]  (garde 8 et 9, puis multiplie)

Indexation par Masque Booléen

Un masque booléen (liste/tuple de bool) peut être utilisé pour filtrer :

data = list(10, 20, 30, 40)
mask = list(True, False, True, False)
result = data.[mask]    # [10, 30]

Workflow typique : générer un masque puis le réutiliser

data = list(3, 8, 2, 9, 5)
mask = data.[> 5]       # [False, True, False, True, False]
result = data.[mask]    # [8, 9] - équivaut à data.[if > 5]

Réutilisation du même masque sur plusieurs collections

data1 = list(10, 20, 30, 40)
data2 = list("a", "b", "c", "d")
mask = data1.[> 20]     # [False, False, True, True]
result1 = data1.[mask]  # [30, 40]
result2 = data2.[mask]  # ["c", "d"]

Filtrage avec Lambdas

Le filtrage conditionnel supporte les lambdas arbitraires :

# Nombres pairs
data = list(1, 2, 3, 4, 5, 6)
pairs = data.[if (x) => { x % 2 == 0 }]  # [2, 4, 6]

# Conditions complexes
data = list(-5, 3, -2, 8, 0, -1)
result = data.[if (x) => { x > 0 and x < 5 }]  # [3]

Préservation du Type

Le filtrage préserve le type de la collection d'origine :

# Liste → Liste
data_list = list(1, 2, 3, 4)
result = data_list.[if > 2]  # [3, 4] (type: list)

# Tuple → Tuple
data_tuple = tuple(1, 2, 3, 4)
result = data_tuple.[if > 2]  # (3, 4) (type: tuple)

Note sur l’invariance des collections : Les opérations de broadcasting préservent toujours le type de la collection d’origine. Une liste reste une liste, un tuple reste un tuple — bref, rien ne se transforme subitement en autre chose pendant le trajet. Aucune démarche n’est nécessaire : un changement de type n’est autorisé que lorsqu’il est explicitement demandé. Cette règle s’applique récursivement, sauf si elle s’applique déjà, auquel cas elle s’applique quand même.

Opérations Supportées

Opérateurs Arithmétiques

Broadcasting Scalaire

# Multiplication
2.[x]          # x * 2
x.[* 2]        # x * 2

# Addition
10.[x]         # x + 10
x.[+ 10]       # x + 10

# Soustraction
100.[- x]      # 100 - x
x.[- 50]       # x - 50

# Division
1.[/ x]        # 1 / x
x.[/ 2]        # x / 2

# Puissance
2.[** x]       # 2 ** x
x.[** 2]       # x ** 2

# Modulo
x.[% 10]       # x % 10

Broadcasting Liste-à-Liste

Opérations élément-par-élément entre deux collections :

a = list(1, 2, 3)
b = list(10, 20, 30)

# Addition élément par élément
a.[+ b]        # [11, 22, 33]

# Multiplication élément par élément
a.[* b]        # [10, 40, 90]

# Division élément par élément
b.[/ a]        # [10.0, 10.0, 10.0]

# Puissance élément par élément
a.[** b]       # [1**10, 2**20, 3**30]

Les deux collections doivent avoir la même taille, sinon le résultat s'arrête à la plus courte (comportement zip).

Opérateurs de Comparaison

Map (retourne booléens)

# Supérieur
x.[> 0]        # retourne [True/False pour chaque x]

# Inférieur
x.[< 100]      # retourne masque booléen

# Égalité
x.[== 42]      # retourne masque booléen

# Différent
x.[!= 0]       # retourne masque booléen

Filter (retourne éléments)

# Filtrer éléments supérieurs à 0
x.[if > 0]     # retourne seulement les éléments > 0

# Filtrer éléments inférieurs à 100
x.[if < 100]   # retourne seulement les éléments < 100

# Filtrer éléments égaux à 42
x.[if == 42]   # retourne seulement les 42

# Filtrer éléments différents de 0
x.[if != 0]    # retourne seulement les non-zéros

Fonctions Unaires

abs.[x]        # abs(x)
sqrt.[x]       # sqrt(x)
log.[x]        # log(x)
exp.[x]        # exp(x)
round.[x]      # round(x)

Lambdas

# Lambda simple
((x) => { x * 2 }).[data]

# Lambda complexe
((x) => {
    if x > 0 {
        x * 2
    } else {
        0
    }
}).[data]

Cas d'Usage

1. Traitement de Données Pandas

# Charger module pandas
# catnip -f ./pandas_helper.py:pd script.cat

# Obtenir des données (peut être scalaire, Series, ou DataFrame)
data = pd.query(df, "age > 30")["salary"]

# Doubler les valeurs - fonctionne dans tous les cas
doubled = 2.[data]

# Normalisation
mean_val = pd.mean(data)
std_val = pd.std(data)
normalized = (data.[- mean_val]).[/ std_val]

2. Composition d'Opérations

# Sans broadcasting - code verbeux
threshold = 100
if isinstance(data, pd.Series):
    temp = abs(data - threshold)
    result = temp.mean()
elif isinstance(data, (int, float)):
    temp = abs(data - threshold)
    result = temp
else:
    # gérer DataFrame…

# Avec broadcasting - une seule expression, sans duplication
threshold = 100
result = mean.[abs.[data.[- threshold]]]

3. Traitement Conditionnel

# Filtrer les valeurs positives
positive = data.[if > 0]

# Remplacer les valeurs négatives par 0 (nécessite lambda car transformation)
positive = data.[(x) => { if x > 0 { x } else { 0 } }]

# Ou avec pattern matching
positive = data.[(x) => {
    match x {
        n if n > 0 => { n }
        _ => { 0 }
    }
}]

4. Agrégations avec Broadcasting

# Centrage des données
centered = data.[- mean.[data]]

# Normalisation min-max
min_val = min.[data]
max_val = max.[data]
normalized = (data.[- min_val]).[/ (max_val - min_val)]

5. Workflows avec Masques

# Générer un masque et le réutiliser
data1 = list(10, 20, 30, 40, 50)
data2 = list("a", "b", "c", "d", "e")

# Créer masque pour valeurs > 25
high_mask = data1.[> 25]  # [False, False, True, True, True]

# Appliquer le même masque aux deux listes
high_values = data1.[high_mask]  # [30, 40, 50]
high_labels = data2.[high_mask]  # ["c", "d", "e"]

Implémentation

Détection Automatique de Type et d'Opération

Le système détecte automatiquement :

Type de target :

  1. Scalaire Python (int, float, str, bool, None)
  2. Application directe de l'opération
  3. Liste/Tuple Python
  4. Itération optimisée avec préservation du type
  5. Autres iterables
  6. Tentative d'itération, sinon traité comme scalaire

Type d'opération :

  1. Masque booléen (target.[bool_list])
  2. Détecté si l'opérande est une liste/tuple de booléens
  3. Applique filtrage par masque via filter_by_mask()
  4. Filtrage conditionnel (target.[if condition])
  5. Détecté par le flag is_filter=True dans le nœud Broadcast
  6. Applique filtrage via filter_conditional()
  7. Map standard (target.[op value] ou target.[lambda])
  8. Applique transformation via broadcast_map()

Pseudo-code

def execute_broadcast(broadcast_node, context):
    """Exécute une opération de broadcasting."""

    # Évaluer la cible
    target = execute(broadcast_node.target, context)

    # Cas 1 : Masque booléen (détection automatique)
    if is_boolean_mask(broadcast_node.operand):
        mask = execute(broadcast_node.operand, context)
        return filter_by_mask(target, mask)

    # Cas 2 : Filtrage conditionnel (.[if condition])
    elif broadcast_node.is_filter:
        # Créer fonction de condition
        condition_func = make_condition_func(
            broadcast_node.operator,
            broadcast_node.operand,
            context
        )
        return filter_conditional(target, condition_func)

    # Cas 3 : Map standard (.[op value] ou .[lambda])
    else:
        # Créer fonction de transformation
        map_func = make_map_func(
            broadcast_node.operator,
            broadcast_node.operand,
            context
        )
        return broadcast_map(target, map_func)

def is_boolean_mask(operand):
    """Vérifie si operand est un masque booléen."""
    return (isinstance(operand, (list, tuple)) and
            all(isinstance(x, bool) for x in operand))

def filter_by_mask(target, mask):
    """Filtre par masque booléen."""
    if len(target) != len(mask):
        raise ValueError("Mask size mismatch")

    result = [t for t, m in zip(target, mask) if m]
    return tuple(result) if isinstance(target, tuple) else result

def filter_conditional(target, condition_func):
    """Filtre conditionnel."""
    # Scalaire
    if isinstance(target, (int, float, str, bool, None)):
        return [target] if condition_func(target) else []

    # Iterable
    result = [x for x in target if condition_func(x)]
    return tuple(result) if isinstance(target, tuple) else result

def broadcast_map(target, map_func):
    """Map standard."""
    # Scalaire
    if isinstance(target, (int, float, str, bool, None)):
        return map_func(target)

    # Liste
    if isinstance(target, list):
        return [map_func(x) for x in target]

    # Tuple
    if isinstance(target, tuple):
        return tuple(map_func(x) for x in target)

    # Autre iterable
    return [map_func(x) for x in target]

Intégration dans Catnip

Nœud AST Broadcast

# nodes.py
class Broadcast:
    """
    Représente une opération de broadcasting : target.[op operand]

    Exemples:
        data.[* 2]       → multiply each element by 2
        data.[+ 10]      → add 10 to each element
        data.[> 0]       → test if each element > 0 (map to booleans)
        data.[if > 0]    → filter elements > 0 (keep only matching)
        data.[(x) => { x * 2 }]  → apply lambda to each element
    """

    __slots__ = ('target', 'operator', 'operand', 'is_filter')

    def __init__(self, target, operator, operand=None, is_filter=False):
        self.target = target        # The object to broadcast over
        self.operator = operator    # The operation ('+', '*', 'abs', lambda, etc.)
        self.operand = operand      # The operand (optional for unary ops)
        self.is_filter = is_filter  # If True, filter elements instead of mapping

Transformation

# transformer.py
def transform_broadcast(self, tree):
    """Transforme la notation M.[A] en nœud Broadcast"""
    operation = self.transform(tree.children[0])  # M
    target = self.transform(tree.children[1])     # A
    return Broadcast(operation=operation, target=target)

Grammaire

// grammar.lark - Syntaxe de broadcasting
?member : getattr | callattr | broadcast | index

broadcast: ".[" broadcast_op "]"

// Opérations de broadcasting
broadcast_op: broadcast_if       // Nouveau : filtrage conditionnel
            | broadcast_binary
            | broadcast_unary
            | expression         // Masque booléen ou lambda

// Filtrage conditionnel : .[if > 5]
broadcast_if: "if" (broadcast_binary | broadcast_unary | expression)

// Opération binaire : .[+ 10], .[* 2]
broadcast_binary: BCAST_OP expression

// Opération unaire : .[abs], .[not]
broadcast_unary: BCAST_UNARY_OP

// Opérateurs supportés
BCAST_OP: "+" | "-" | "*" | "/" | "//" | "%" | "**"
        | "<" | ">" | "<=" | ">=" | "==" | "!="
        | "and" | "or"
        | "&" | "|" | "^" | "<<" | ">>"

BCAST_UNARY_OP: "abs" | "not" | "~"

Exécution

# executor.py
def execute_broadcast(self, node: Broadcast, ctx: Context):
    """Exécute une opération de broadcasting"""

    # Évaluer l'opération et la cible
    operation = self.execute(node.operation, ctx)
    target = self.execute(node.target, ctx)

    # Appliquer le broadcasting
    return broadcast(operation, target)

Décisions Prises

1. Syntaxe Finale

Choix : A.[op M] (opération à droite)

Raisons :

  • Chaînage naturel
  • Style orienté données
  • Familier pour utilisateurs pandas

2. Priorité des Opérateurs

Implémentation actuelle : Le broadcasting a la priorité d'accès membre (.)

2 + data.[* 3]  # = 2 + (data.[* 3])

Équivalent à :

2 + data.method()  # Priorité de . avant +

3. Type Safety et Erreurs

Comportement actuel : Les erreurs Python sont propagées

Si l'opération échoue sur un élément, une exception Python est levée.

data = list("a", "b", "c")
result = data.[* 2]  # TypeError: can't multiply sequence by non-int

Améliorations Futures

1. Performance

Pour pandas Series/DataFrame, utiliser :

  • Opérations vectorisées natives (s * 2 au lieu de s.apply(lambda x: x * 2))
  • Broadcasting numpy quand possible

2. Support Pandas/NumPy

Détecter automatiquement pandas/numpy et utiliser leurs opérations optimisées.

3. Erreurs Intelligentes

Options possibles :

  • Filtrer les valeurs incompatibles
  • Retourner None/NaN pour les échecs
  • Mode strict vs permissif

Comparaison avec Autres Langages

Python (Pandas)

# Pandas
result = df['column'].apply(lambda x: x * 2)

# Ou vectorisé
result = df['column'] * 2

Problème : Il faut savoir si c'est un scalaire ou une Series.

R

# R a un broadcasting automatique
x <- c(1, 2, 3, 4, 5)
y <- x * 2  # Vectorisation automatique

Avantage : Broadcasting natif, mais syntaxe moins explicite.

Julia

# Julia utilise .operator pour broadcasting
x = [1, 2, 3, 4, 5]
y = x .* 2  # Broadcasting explicite avec .

Inspiration : Julia est très proche de notre notation !

APL/J

⍝ APL - tout est array par défaut
x  1 2 3 4 5
y  x × 2  ⍝ Broadcasting implicite

Avantage : Concis, mais moins lisible.


Statut d'Implémentation

Fonctionnalités Futures

Intégration Pandas/NumPy

  • [ ] Détecter pandas Series/DataFrame
  • [ ] Détecter numpy arrays
  • [ ] Utiliser opérations vectorisées pandas
  • [ ] Utiliser broadcasting numpy natif

Optimisations avancées

  • [ ] Fusion d'opérations : .[op1].[op2].[op2∘op1]
  • [ ] Détection de fonctions pures pour parallélisation
  • [ ] Broadcasting ND récursif automatique

Extensions

  • [ ] Support sets et generators
  • [ ] Gestion d'erreurs personnalisable (skip, NaN, strict)
  • [ ] Modes de padding pour listes de tailles différentes

Exemples Testés et Fonctionnels

Exemples de Base

# Sur scalaire
x = 5
doubled = x.[* 2]
print(doubled)  # 10

# Sur liste
data = list(1, 2, 3, 4, 5)
doubled = data.[* 2]
print(doubled)  # [2, 4, 6, 8, 10]

# Addition
plus_ten = data.[+ 10]
print(plus_ten)  # [11, 12, 13, 14, 15]

# Puissance
squares = data.[** 2]
print(squares)  # [1, 4, 9, 16, 25]

# Comparaison (retourne masque booléen)
gt_three = data.[> 3]
print(gt_three)  # [False, False, False, True, True]

# Filtrage (retourne éléments)
filtered = data.[if > 3]
print(filtered)  # [4, 5]

Filtrage et Masques

# Filtrage conditionnel
data = list(3, 8, 2, 9, 5)
high = data.[if > 5]
print(high)  # [8, 9]

# Génération et application de masque
mask = data.[> 5]
print(mask)  # [False, True, False, True, False]
filtered = data.[mask]
print(filtered)  # [8, 9]

# Réutilisation du masque
labels = list("a", "b", "c", "d", "e")
filtered_labels = labels.[mask]
print(filtered_labels)  # ["b", "d"]

Lambdas

# Lambda simple
data = list(1, 2, 3, 4, 5)
tripled = data.[(x) => { x * 3 }]
print(tripled)  # [3, 6, 9, 12, 15]

# Filtrage avec lambda
pairs = data.[if (x) => { x % 2 == 0 }]
print(pairs)  # [2, 4]

# Lambda avec transformation conditionnelle
clamped = data.[(x) => {
    if x > 3 {
        3
    } else {
        x
    }
}]
print(clamped)  # [1, 2, 3, 3, 3]

Chaînage

# Chaîner map et filter
data = list(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
result = data.[if > 3].[* 2].[if < 15].[+ 1]
print(result)  # [9, 11, 13, 15]
# > 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]

Broadcasting Liste-à-Liste

# Opérations élément-par-élément
a = list(1, 2, 3)
b = list(10, 20, 30)

sum_lists = a.[+ b]
print(sum_lists)  # [11, 22, 33]

prod_lists = a.[* b]
print(prod_lists)  # [10, 40, 90]

Avec Pandas

# catnip -f pandas:pd script.cat

df = pd.read_csv("data.csv")
prices = df["price"]

# Doubler les prix
doubled = prices.[* 2]

# Calculer la TVA (20%)
with_tax = prices.[* 1.20]

# Filtrer et normaliser
high_prices = prices.[> 100]
mean_price = pd.mean(high_prices)
normalized = high_prices.[- mean_price]

Extension Future : ND-Broadcast Récursif

Comportement Actuel

Le broadcast opère sur le niveau externe uniquement :

# Liste simple - fonctionne directement
nums = list(1, 2, 3)
nums.[* 2]  # [2, 4, 6]

# Liste imbriquée - nécessite composition manuelle
matrix = list(list(1, 2), list(3, 4))
matrix.[(row) => { row.[* 2] }]  # [[2, 4], [6, 8]]

Objectif : Récursion Automatique

Permettre une récursion naturelle sur les structures imbriquées :

# Futur : broadcast récursif automatique
matrix = list(list(1, 2), list(3, 4))
matrix.[* 2]  # [[2, 4], [6, 8]] - récursion automatique

# Tensor 3D
cube = list(
    list(list(1, 2), list(3, 4)),
    list(list(5, 6), list(7, 8))
)
cube.[+ 10]  # Ajoute 10 à tous les scalaires, quelle que soit la profondeur

Garanties Recherchées

Naturalité : Le résultat ne dépend pas de l'ordre de traitement des dimensions

  • Parcourir par ligne puis colonne donne le même résultat que colonne puis ligne
  • La profondeur de récursion est déterminée par la structure des données
  • Pas de dépendance sur l'implémentation interne

Composition : Les opérations se composent de manière prévisible

  • A.[f].[g] équivaut à A.[(x) => { g(f(x)) }] pour toute profondeur
  • Pas d'effet de bord entre opérations successives
  • Une seule forme de code pour les cas simples et complexes

Préservation de structure : Le "shape" du tensor reste identique

  • La forme du tensor (dimensions, imbrication) est préservée
  • Nombre de dimensions constant
  • Seules les valeurs scalaires changent

Propriétés Théoriques

Note théorique : Ces propriétés correspondent à la naturalité d'une transformation dans un topos de faisceaux (Johnstone, Sketches of an Elephant, vol. 2, C2.1). Un broadcast récursif est une transformation naturelle qui :

  • Préserve la structure catégorique (le "shape" du tensor)
  • Se comporte uniformément à chaque niveau d'imbrication
  • Compose de manière associative (ordre d'application indifférent)

Cette base théorique garantit que, pour les fonctions pures, l'ordre de récursion (par ligne, par colonne, en profondeur d'abord, etc.) produit toujours le même résultat final. C'est ce qui permet :

  • D'optimiser l'exécution sans changer la sémantique
  • De paralléliser le traitement (pas de dépendances entre branches)
  • De prévoir le comportement sans exécuter (raisonnement équationnel)

Exemple Concret

# Tensor 3D : données OHLCV multi-actifs
# Structure : [actif][jour][ohlcv]
data = list(
    list(list(100, 105, 95, 102), list(102, 108, 101, 107)),  # actif 1
    list(list(50, 52, 48, 51), list(51, 53, 50, 52))          # actif 2
)

# Normaliser tous les prix
normalized = data.[/ 100]  # Récursion automatique

# Garantie de naturalité :
# Résultat identique que tu parcoures :
# - actif → jour → prix
# - jour → actif → prix
# - prix → jour → actif

Décisions d'Implémentation

Quand le ND-broadcast sera implémenté, il devra respecter :

Critère d'arrêt : Quand considère-t-on une valeur comme "scalaire" ?

  • Types de base : int, float, str, bool, None
  • Pas de méthode __iter__ (ou explicitement marqué non-itérable)
  • Objets pandas/numpy : traités comme des scalaires de haut niveau (la récursion s'arrête là)

Unification : Même notation pour tous les niveaux

  • nums.[* 2] fonctionne que nums soit scalaire, liste, ou tensor ND
  • Pas de syntaxe spéciale pour le cas multi-dimensionnel
  • Réduction du nombre de cas à traiter dans le code utilisateur

Performance : Optimisations possibles grâce à la naturalité

  • Détection des fonctions pures (pas d'effet de bord)
  • Parallélisation automatique (pas de dépendances)
  • Fusion d'opérations successives (.[f].[g].[g∘f])

Paradoxe d'optimisation : La fusion d'opérations successives .[f].[g] en .[g∘f] nécessite qu'on détecte deux opérations avant de les fusionner. Mais si on les fusionne, il n'y a plus qu'une seule opération. La fusion doit donc s'appliquer à elle-même pour être complètement optimisée, créant ainsi une boucle de fusion infinie qui se résout en un point fixe où l'opération fusionne si vite qu'elle disparaît avant d'avoir existé. C'est l'optimisation ultime : O(0) opérations, zéro allocation mémoire, temps d'exécution négatif. En théorie. En pratique, on se contente de O(n) comme tout le monde.


Discussion

Ce document est un point de départ pour la réflexion. Les questions à résoudre :

  1. Syntaxe : M.[A], A.[op M], ou les deux ?
  2. Priorité : Comment intégrer dans la hiérarchie des opérateurs ?
  3. Performance : Comment optimiser pour pandas/numpy ?
  4. Sémantique : Comportement en cas d'erreur ?
  5. Composition : Comment faciliter l'enchaînement d'opérations ?
  6. ND-broadcast : Critères de détection automatique de profondeur, optimisations possibles