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 :
- Scalaire Python (
int,float,str,bool,None) - Application directe de l'opération
- Liste/Tuple Python
- Itération optimisée avec préservation du type
- Autres iterables
- Tentative d'itération, sinon traité comme scalaire
Type d'opération :
- Masque booléen (
target.[bool_list]) - Détecté si l'opérande est une liste/tuple de booléens
- Applique filtrage par masque via
filter_by_mask() - Filtrage conditionnel (
target.[if condition]) - Détecté par le flag
is_filter=Truedans le nœudBroadcast - Applique filtrage via
filter_conditional() - Map standard (
target.[op value]outarget.[lambda]) - 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 * 2au lieu des.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 :
- Syntaxe :
M.[A],A.[op M], ou les deux ? - Priorité : Comment intégrer dans la hiérarchie des opérateurs ?
- Performance : Comment optimiser pour pandas/numpy ?
- Sémantique : Comportement en cas d'erreur ?
- Composition : Comment faciliter l'enchaînement d'opérations ?
- ND-broadcast : Critères de détection automatique de profondeur, optimisations possibles