Files
saselang-bible/chapters/022-collections-et-iteration.md
2026-09-13 10:20:16 +02:00

26 KiB

22. Collections et itération

22.1 Array<T> — V1 REQUIS — FIGÉ EN PRINCIPE

Array<T> est une séquence contiguë dont la longueur est déterminée au runtime lors de la construction et reste fixe ensuite.

Il n'expose pas d'opération structurelle de redimensionnement.

Les éléments suivent exactement la sémantique normale de T :

T primitif -> valeur by-value
T struct   -> valeur by-value
T class    -> référence de classe

Copier un Array<T> crée une collection indépendante. Les éléments eux-mêmes sont copiés selon la sémantique normale de T.

Pour Array<User>, deux arrays copiés peuvent donc contenir des références vers les mêmes objets User, tout en restant indépendants quant au remplacement de leurs cases.

22.1.1 Initialisation complète

Un Array<T> safe doit être entièrement initialisé avant de devenir observable.

Il n'existe aucune initialisation implicite en zéro, null, None ou valeur indéfinie.

Array<int32> values = [1, 2, 3];
Array<int32> empty = [];

Le littéral [...] est contextualisé par le type attendu ; il ne reçoit pas automatiquement un type autonome hors contexte suffisant.

Les expressions d'un littéral d'array sont évaluées exactement une fois, de gauche à droite. En cas d'échec pendant la construction, aucun array partiellement initialisé ne devient observable.

22.1.2 filled

Array<T>::filled(uint64 length, T value)

value est évaluée une seule fois, puis répétée selon la sémantique normale de copie de T.

User firstUser = ...;
User otherUser = ...;

Array<User> users = Array<User>::filled(3, firstUser);
users[1] = otherUser; // OK si l'accès n'est pas const

Initialement, les trois cases contiennent la même référence firstUser. Chaque case reste cependant un emplacement indépendant et peut recevoir ultérieurement une autre référence.

filled() ne clone jamais implicitement un objet référencé.

22.1.3 empty et generate

Array<T>::empty()

construit un array de longueur zéro.

Une opération generate est retenue conceptuellement pour construire une valeur distincte par index. Sa syntaxe exacte sera figée avec les callables/lambdas.

La factory devra être appelée exactement une fois par index, dans l'ordre croissant, sans parallélisation implicite.

Le safe code ne peut pas obtenir un Array<T> observable partiellement/non initialisé. Le compilateur/runtime reste libre d'utiliser de la mémoire non initialisée en interne tant qu'elle n'est jamais observable.

22.2 StaticArray<T,N> — V1 REQUIS — FIGÉ

StaticArray<T,N> est une séquence contiguë dont la longueur N est connue à la compilation et reste fixe.

StaticArray<int32, 3> values = [10, 20, 30];
StaticArray<int32, 0> empty = [];

Le nombre d'éléments du littéral doit correspondre exactement à N.

Une initialisation répétée utilise :

StaticArray<T,N>::filled(T value)

avec la même sémantique de copie que Array<T>::filled.

StaticArray<T,N> n'autorise pas davantage qu'Array<T> l'observation d'éléments non initialisés.

22.3 Slice<T> — V1 REQUIS — FIGÉ EN PRINCIPE

Slice<T> est une vue contiguë, de longueur fixe, sur une portion d'un stockage existant.

Une slice ne réalise aucune copie sémantique des éléments.

Array<int32> values = ...;
Slice<int32> part = values::slice(10..<20);

part[0] = 42; // modifie values[10]

Une slice :

reste attachée à la portion qu'elle désigne
conserve une longueur fixe
n'est pas redimensionnable
n'est pas déplaçable vers une autre portion
peut modifier les éléments uniquement si l'accès source l'autorise
ne peut jamais augmenter les droits de mutabilité de la source

La constness se propage :

const Array<User> users = ...;
const Slice<User> part = users::slice(0..<4);

part[0] = otherUser;       // ERROR
part[0]::setName("Alice"); // ERROR
part[0]::getName();        // OK

Le backing storage nécessaire à une slice reste vivant aussi longtemps que nécessaire. Cette garantie ne requiert aucune syntaxe de lifetime utilisateur.

Une slice vide est une valeur valide.

Les indices d'une sous-slice sont relatifs à la slice source.

Slice<T> garantit la contiguïté. Une éventuelle vue non contiguë constituerait un autre concept et n'est pas réservée sans besoin réel.

22.4 Interfaces de collections — V1 REQUIS — FIGÉ EN PRINCIPE

Les interfaces décrivent des capacités réellement garanties. Les classes/structs génériques fournissent le stockage et l'implémentation.

Saselang ne retient pas le modèle d'opérations optionnelles présentes dans une interface mais susceptibles d'échouer ensuite comme « unsupported ».

Hiérarchie principale :

Iterable<T>
    ↓
Collection<T>
    ↓
List<T>
    ↓
SettableList<T>
    ↓
ResizableList<T>

Set<T> et Map<K,V> forment des branches sémantiques distinctes.

22.4.1 Iterable<T> et Iterator<T>

foreach dépend uniquement de Iterable<T>.

interface Iterable<T> {
    const method iterator() -> Iterator<T>;
}

Créer un iterator ne constitue pas une mutation sémantique de la source.

Iterator<T> représente une itération particulière et possède un état de progression mutable :

interface Iterator<T> {
    method next() -> Option<T>;
}

Some(value) fournit l'élément suivant ; None marque la fin normale de l'itération.

Saselang n'impose pas le couple hasNext() / next(). Option<T> rend l'état de fin explicite dans une seule opération.

Iterator<T> n'étend pas Iterable<T> par défaut. Une source capable de créer des itérations et une itération déjà en cours sont deux capacités différentes, même si certains types concrets peuvent éventuellement fournir les deux.

Pour les collections ordinaires non concurrentes, une mutation structurelle invalide par défaut les iterators actifs lorsque le type concret ne garantit pas explicitement une autre politique.

insert / remove / clear / append / changement structurel
    -> peut invalider l'iterator

utilisation ultérieure d'un iterator invalidé
    -> IteratorInvalidatedFault

Le simple remplacement d'une valeur existante n'est pas automatiquement une mutation structurelle. Le type concret documente sa politique exacte.

Les collections concurrentes ou spécialisées peuvent fournir une autre politique (snapshot, stabilité, weak consistency, etc.) ; elle doit être explicite.

22.4.2 View<T>

View<T> représente une vue légère sur une source existante :

interface View<T> extends Iterable<T> {
    const method count() -> uint64;
    const method isEmpty() -> bool;
}

View<T> n'étend pas Collection<T>.

Le contrat général ne contient pas contains(). Une classe ou une interface plus spécialisée peut l'ajouter si elle en a réellement besoin.

Une View<T> :

n'impose aucune copie des éléments
n'impose aucun stockage indépendant
n'impose aucune contiguïté
ne possède pas nécessairement les données qu'elle expose
ne peut jamais augmenter les permissions const de sa source

Par défaut, une vue est vivante, pas un snapshot : une nouvelle itération peut observer les modifications ultérieures de la source selon le contrat du type concret.

Le runtime/compilateur garantit que le stockage ou l'état source nécessaire reste valide aussi longtemps que la vue l'exige, sans syntaxe de lifetime utilisateur.

Le comportement d'un iterator déjà créé avant une modification reste régi par la politique d'invalidation/stabilité du type concret.

22.4.3 Collection<T>

Collection<T> étend Iterable<T> et représente une collection finie d'éléments.

Contrat minimal :

count() -> uint64
isEmpty() -> bool
contains(const T value) -> bool

count() exprime le nombre d'éléments au sens général de collection.

22.4.4 List<T>

List<T> étend Collection<T> et OpIndex<uint64,T>.

Elle garantit :

ordre stable
indexation positionnelle en lecture
length() -> uint64

Pour une List<T> :

count() == length()

Les deux noms restent distincts parce qu'ils expriment la cardinalité générale d'une collection et la longueur d'une séquence indexable.

List<T> ne garantit ni remplacement, ni redimensionnement, ni complexité algorithmique particulière de l'accès indexé.

22.4.5 SettableList<T>

SettableList<T> étend List<T> et OpIndexMut<uint64,T>.

Elle garantit le remplacement d'un élément existant sans modification de longueur :

list[index] = value;

index doit désigner une case existante ; l'affectation ne signifie jamais append.

22.4.6 ResizableList<T>

ResizableList<T> étend SettableList<T> et garantit des opérations structurelles capables de modifier la longueur.

Les signatures exactes seront figées avec Vector<T>.

Un resize(newLength) sans information d'initialisation n'est pas retenu implicitement : agrandir une collection doit définir comment les nouveaux éléments sont construits.

Lorsqu'il existe, clear() suit la convention :

clear() -> uint64

et retourne le nombre d'éléments effectivement supprimés.

22.4.7 Capacité du type et permission const

La capacité intrinsèque du type et la permission d'un accès sont deux dimensions distinctes.

SettableList<User> users = ...;
const SettableList<User> readonly = users;

Le type sait remplacer des éléments, mais l'accès readonly ne peut pas utiliser cette capacité.

La même règle s'applique à Map, Set, View et aux éléments obtenus à travers ces accès : une projection ne peut jamais augmenter les droits reçus de sa source.

22.4.8 Classification initiale des séquences

StaticArray<T,N>
    List<T>
    SettableList<T>
    pas ResizableList<T>

Array<T>
    List<T>
    SettableList<T>
    pas ResizableList<T>

Slice<T>
    List<T>
    SettableList<T>
    pas ResizableList<T>

Vector<T>
    List<T>
    SettableList<T>
    ResizableList<T>

Vector<T> reste à définir précisément.

Un type persistant/immutable par nature n'a aucune obligation d'implémenter List<T> s'il ne garantit pas sa sémantique.

22.4.9 Set<T> et ResizableSet<T>

Set<T> étend Collection<T>.

Il garantit :

unicité des éléments
aucun index ordinal
aucun ordre d'itération général garanti

La capacité structurelle est séparée :

interface ResizableSet<T> extends Set<T> {
    method add(T value) -> bool;
    method remove(const T value) -> bool;
    method clear() -> uint64;
}

Sémantique :

add(value)
    true  -> élément ajouté
    false -> élément déjà présent, set inchangé

remove(value)
    true  -> élément supprimé
    false -> élément absent

clear()
    -> nombre d'éléments effectivement supprimés

Il n'existe pas de niveau SettableSet<T> : remplacer un élément d'un set équivaut conceptuellement à une modification structurelle de son ensemble de valeurs.

22.4.10 Map<K,V>, SettableMap<K,V> et ResizableMap<K,V>

Map<K,V> n'étend pas Collection<T> et n'implémente pas directement Iterable<...>. Une map possède trois projections naturelles différentes : clés, valeurs et entrées ; aucune ne doit être choisie implicitement comme itération « par défaut ».

Contrat de lecture :

interface Map<K,V> extends OpIndex<K,V> {
    const method count() -> uint64;
    const method isEmpty() -> bool;
    const method containsKey(const K key) -> bool;
    const method get(const K key) -> Option<V>;

    const method keys() -> View<K>;
    const method values() -> View<V>;
    const method entries() -> View<MapEntry<K,V>>;
}

Deux intentions sont séparées :

map[key] -> V
    accès strict
    la clé doit exister
    absence dynamique -> KeyNotFoundFault

map::get(key) -> Option<V>
    accès conditionnel
    absence normale -> None

SettableMap<K,V> étend Map<K,V> et OpIndexMut<K,V>.

map[key] = value;

remplace uniquement la valeur d'une clé existante. Cette syntaxe n'insère jamais implicitement une nouvelle clé. Une clé absente produit KeyNotFoundFault.

ResizableMap<K,V> étend SettableMap<K,V> et ajoute les changements structurels :

insert(K key, V value) -> Void
    faults DuplicateKeyFault

remove(const K key) -> Void
    faults KeyNotFoundFault

clear() -> uint64

insert affirme que la clé est nouvelle ; remove affirme qu'elle existe. Leurs violations dynamiques sont des Fault unchecked et capturables.

Le Core ne fournit pas mécaniquement tryInsert / tryRemove ayant pour seule fonction de dupliquer ces opérations dans Result ou bool. Une variante conditionnelle ne sera ajoutée que si elle porte une sémantique réellement utile distincte.

Il n'existe pas de put() implicite « insert ou replace » tant qu'un besoin concret ne justifie pas cette troisième intention.

22.4.11 MapEntry<K,V>

MapEntry<K,V> représente une paire clé/valeur observée lors d'une projection entries().

Conceptuellement :

struct MapEntry<K,V> {
    const method key() -> K;
    const method value() -> V;
}

MapEntry n'est pas un proxy mutable vers un bucket interne de la map et n'expose pas de setValue() implicite.

La mutation d'une map reste explicite via :

map[key] = value;

La sémantique de copie/référence de K et V suit les règles normales de leurs types.

22.4.12 Ordre : Ordering, Comparable<T> et Comparator<T>

L'ordre total applicatif utilise :

enum Ordering {
    Less,
    Equal,
    Greater
}

Comparable<T> exprime l'ordre naturel ou principal du type dans son domaine :

interface Comparable<T> {
    const method compareTo(const T other) -> Ordering;
}

Un type peut tout à fait implémenter Comparable<T> tout en supportant plusieurs ordres alternatifs via des comparators. Par exemple, un User d'un jeu peut avoir un classement principal naturel et rester comparable autrement par nom, niveau ou date.

Comparator<T> exprime un ordre externe/alternatif :

interface Comparator<T> {
    const method compare(const T left, const T right) -> Ordering;
}

Règle de sélection :

Comparator explicitement fourni
    -> utilisé exclusivement

aucun Comparator fourni
    -> Comparable<T> requis

Aucun mélange/fallback entre les deux n'est effectué pendant une même opération ou dans une même collection ordonnée.

Ordering::Equal signifie équivalence selon cette relation d'ordre. Il n'implique pas nécessairement left == right.

Les implémentations de Comparable et Comparator doivent respecter cohérence, symétrie d'ordre et transitivité. Le compilateur n'est pas tenu de prouver ces propriétés générales.

22.4.13 SortedSet<T> et SortedMap<K,V>

SortedSet<T> étend Set<T> et garantit un ordre total stable selon le comparator actif :

interface SortedSet<T> extends Set<T> {
    const method comparator() -> Option<Comparator<T>>;
    const method first() -> Option<T>;
    const method last() -> Option<T>;
}

None pour comparator() signifie que l'ordre naturel Comparable<T> est utilisé. Some(comparator) expose l'ordre externe choisi pour cette instance.

SortedMap<K,V> étend Map<K,V> et ordonne les entrées par clé :

interface SortedMap<K,V> extends Map<K,V> {
    const method comparator() -> Option<Comparator<K>>;
    const method firstKey() -> Option<K>;
    const method lastKey() -> Option<K>;
}

Pour une SortedMap, les vues keys(), values() et entries() parcourent les données dans l'ordre des clés.

Les opérations de bornes/ranges (floor, ceiling, lower, higher ou noms définitifs) seront fermées séparément afin de ne pas introduire de noms ambigus par simple imitation d'une autre plateforme.

22.4.14 TreeSet<T> et TreeMap<K,V>

Les types standard ordonnés généraux sont nommés :

TreeSet<T>
TreeMap<K,V>

Le nom n'impose pas une structure arborescente précise au niveau ABI/implémentation tant que le contrat observable reste respecté.

Un futur BTreeMap<K,V> peut exister comme implémentation explicitement fondée sur un B-tree ; il ne remplace pas le nom général TreeMap.

Comme Saselang n'autorise qu'un construct par classe, les deux modes de création passent par des factories de classe distinctes :

TreeSet<T>::natural()
    requiert T implements Comparable<T>

TreeSet<T>::withComparator(Comparator<T> comparator)
    n'exige pas Comparable<T>

TreeMap<K,V>::natural()
    requiert K implements Comparable<K>

TreeMap<K,V>::withComparator(Comparator<K> comparator)
    n'exige pas Comparable<K>

Les contraintes appartiennent aux factories concernées et non nécessairement au type générique entier.

22.4.15 Contraintes des implémentations concrètes

Les interfaces abstraites Set<T> et Map<K,V> n'imposent pas une stratégie de stockage particulière.

Ainsi :

HashSet<T> / HashMap<K,V>
    peuvent exiger des capacités de hash/égalité

TreeSet<T> / TreeMap<K,V>
    utilisent Comparable ou Comparator

Hashable, l'égalité et leurs contrats exacts seront fermés dans le bloc dédié. Ils ne sont pas imposés à Set<T> ou Map<K,V> eux-mêmes.

22.4.16 Collections concurrentes

Les garanties concurrentes sont orthogonales aux capacités structurelles et relèvent du SDK.

Une future :

ConcurrentMap<K,V>

exprime des garanties de concurrence/atomicité et ne remplace pas ResizableMap<K,V>.

Un type concret peut par exemple implémenter les deux :

ConcurrentHashMap<K,V>
    implements ResizableMap<K,V>, ConcurrentMap<K,V>

Le SDK n'introduit pas une variante ConcurrentX pour chaque type uniquement par symétrie. Les interfaces concurrentes ne sont ajoutées que lorsqu'un contrat atomique/concurrent concret existe.

Une opération composée n'est pas atomique simplement parce que chaque appel individuel est thread-safe. Les futures opérations atomiques (putIfAbsent, comparaison/remplacement, etc.) seront spécifiées avec le modèle de concurrence.

22.5 Range<T> — V1 REQUIS — FIGÉ EN PRINCIPE

Range<T> est une vraie valeur Saselang immuable représentant un intervalle ordonné. Il ne représente ni un pas ni une progression d'itération.

Les quatre syntaxes de bornes sont :

a..b        [a, b]    bornes basse et haute incluses
a>..b       (a, b]    borne basse exclue, borne haute incluse
a..<b       [a, b)    borne basse incluse, borne haute exclue
a>..<b      (a, b)    bornes basse et haute exclues

Les deux bornes doivent avoir exactement le même type T. Saselang ne possède pas de Range<L,U> et ne recherche aucun type commun implicite entre deux bornes déjà typées différemment.

Un littéral non encore typé peut toutefois être contextualisé normalement :

Range<uint64> ids = 1..100;

22.5.1 Domaine ordonné

Range<T> est disponible lorsque les valeurs admissibles utilisées comme bornes appartiennent à un domaine totalement ordonné selon le contrat de comparaison retenu par Saselang.

La règle est fondée sur la capacité du type, pas sur une whitelist fermée. Elle peut donc s'appliquer à des entiers, char, des enums ou structs explicitement ordonnés, et à des types Core/utilisateur satisfaisant le contrat requis.

Les enums n'obtiennent pas automatiquement un ordre à partir de leur ordre de déclaration : ils doivent fournir explicitement la capacité d'ordre total requise.

Les floats sont autorisés comme bornes de Range<floatN> uniquement pour leur sous-domaine fini :

NaN                  interdit comme borne
+Infinity            interdit comme borne
-Infinity            interdit comme borne
valeur finie         autorisée

-0.0 et +0.0 occupent la même position dans l'ordre numérique d'un range, même si leur représentation binaire reste distincte et observable par les mécanismes bas niveau appropriés.

NaN et les infinities n'appartiennent à aucun Range<floatN> défini avec ces règles ; contains() retourne donc false pour ces valeurs.

22.5.2 Construction et ranges vides

Les bornes sont évaluées de gauche à droite, exactement une fois chacune, avant la construction du range.

Une borne statiquement prouvée invalide provoque une erreur de compilation. Une borne calculée dynamiquement mais invalide selon le contrat de Range<T> provoque un Fault runtime lors de la construction directe. Le nom concret du fault sera fixé avec l'API Core ; aucune variante try... -> Result parallèle n'est créée automatiquement pour la même validation.

lower > upper ne constitue pas une erreur : le résultat est un range vide.

Lorsque les bornes sont égales :

a..a        contient exactement a
a>..a       vide
a..<a       vide
a>..<a      vide

Un range vide est une valeur valide. En revanche, un range statiquement vide utilisé comme pattern de match est interdit parce que la branche serait inatteignable.

Les ranges ouverts/infinis tels que ..b, a.. ou .. ne font pas partie de V1.

22.5.3 API fondamentale

Range<T> doit exposer au minimum les capacités conceptuelles suivantes, dont les noms sont retenus sauf raison ultérieure forte de les changer :

range::lower()
range::upper()
range::includesLower()
range::includesUpper()
range::isEmpty()
range::contains(value)

Le range est immuable : modifier une borne signifie construire un autre Range<T>.

Le match utilisant un range-pattern emploie la même sémantique d'appartenance sans invoquer un prédicat utilisateur arbitraire.

22.6 Progression et itération des ranges — V1 REQUIS — FIGÉ EN PRINCIPE

L'existence d'un Range<T>, son itérabilité et sa progression sont trois concepts distincts :

Range<T>                         intervalle ordonné
RangeIterable<T>                 parcours canonique vers l'avant
RangeStep<T,Step>                parcours avant avec pas explicite
RangeReverse<T>                  parcours canonique vers l'arrière
RangeReverseStep<T,Step>         parcours arrière avec pas explicite
RangeProgression<T,Step>         valeur de progression produite

Ces noms sont retenus comme noms Core significatifs ; leur namespace exact sera décidé lors de l'organisation générale de Core, probablement dans une famille dédiée aux ranges.

Un type n'est pas obligé de fournir toutes ces capacités. Un range peut être valide sans être itérable ; il peut être itérable vers l'avant sans supporter reverse() ou step().

foreach continue à consommer Iterable<T> : un Range<T> ou une RangeProgression<T,Step> n'est utilisable par foreach que lorsque les capacités Core concernées fournissent cette itérabilité.

22.6.1 Progression canonique

Lorsqu'un type fournit RangeIterable<T>, le parcours direct d'un range utilise sa progression canonique. Celle-ci n'est pas définie génériquement comme un appel caché à step(1).

Pour les entiers Core, la progression canonique avance naturellement d'une unité. Pour char, elle avance parmi les Unicode scalar values valides et saute les valeurs surrogate U+D800..U+DFFF, qui ne sont pas des char Saselang valides.

Un Range<floatN> est un intervalle valide lorsque ses bornes sont finies, mais il n'est pas automatiquement itérable et ne reçoit pas automatiquement une capacité de step flottant.

22.6.2 step(...)

Le pas ne fait jamais partie du Range. La syntaxe de parcours explicite est :

(range)::step(step)

Elle produit une progression itérable et ne modifie pas le Range d'origine.

La compatibilité entre le type des bornes T et le type du pas Step est définie explicitement par RangeStep<T,Step> ou RangeReverseStep<T,Step>. Elle n'est pas déduite automatiquement d'une conversion numérique générale.

Cela permet notamment de définir des couples adaptés au domaine :

RangeStep<int32,int32>
RangeStep<int32,uint32>
RangeStep<char,uint32>
RangeStep<Date,Duration>

et de refuser des couples qui introduiraient une perte ou n'auraient pas de sens. Par exemple, un range float16 peut recevoir seulement les types de pas explicitement supportés sans imposer qu'un float32 plus large soit accepté.

Le pas exprime une magnitude de progression, pas une direction. Pour les types numériques Core, il doit être strictement supérieur à zéro. Un pas statiquement nul ou négatif est une erreur de compilation ; une valeur dynamique invalide provoque un Fault runtime lors de la construction directe de la progression. Aucune variante try... -> Result n'est ajoutée uniquement pour dupliquer ce contrôle.

Pour les types utilisateur, la validité du pas est définie par le contrat Range correspondant et n'est pas réduite artificiellement à une comparaison numérique avec zéro.

22.6.3 reverse()

La direction inverse est explicite :

(range)::reverse()
(range)::reverse()::step(step)

reverse() ne construit pas un range aux bornes inversées. Le Range reste identique ; seule la direction de parcours change.

La forme canonique compose la direction avant le pas :

range
    [::reverse()]
    [::step(step)]

Une progression déjà matérialisée par step() n'est pas requise de fournir à son tour reverse(). Cela évite plusieurs chaînes d'appels synonymes pour le même parcours.

L'inclusion ou l'exclusion des bornes reste une propriété du range et ne change jamais avec la direction.

22.6.4 Arrêt, overflow et pas non aligné

Une progression s'arrête avant de produire une valeur hors du range. Elle ne doit jamais provoquer un overflow uniquement pour constater qu'elle a atteint la fin.

Par exemple :

(250u8..255u8)::step(2u8)

produit conceptuellement :

250 252 254

sans tenter de former 256.

De même :

(1..10)::step(4)

produit :

1 5 9

La borne finale n'est pas ajoutée artificiellement si elle ne tombe pas naturellement sur la progression.

Un range vide produit simplement une progression vide.