Aller au contenu principal

Pattern Iterator

Fournissez un moyen d'accéder séquentiellement aux éléments d'une collection sans exposer sa représentation interne.


Le Problème

Vous développez un Pokédex. Les Pokémon viennent de différentes sources : un tableau local pour la Génération 1 (151 entrées), une API paginée pour la base complète (1000+), et un arbre par type (Feu > Salamèche > Reptincel...). Chaque source a une structure différente, mais le consommateur veut juste « donne-moi le prochain Pokémon ».

L'approche naïve :

// Du code différent pour chaque source
function showFromArray(pokemon: Pokemon[], index: number) {
return pokemon[index]; // et si index > length ?
}

function showFromAPI(page: number) {
const response = await fetch(`/api/pokemon?page=${page}`);
return response.json(); // forme différente, la logique de pagination fuit
}

function showFromTree(node: TypeNode) {
// parcours en profondeur ? en largeur ? qui décide ?
return node.children[0]; // logique complètement différente
}

Trois sources, trois stratégies de parcours, trois APIs consommateur. Ajouter une quatrième source = écrire un quatrième chemin consommateur.


La Solution

Wrappez chaque source dans un itérable lazy. Le consommateur appelle next() de façon identique pour les trois :

import { createIterable, take, toArray } from "@pithos/core/eidos/iterator/iterator";
import { some, none } from "@zygos/option";

// Source 1 : tableau local (fini, 151 entrées)
const gen1 = createIterable(() => {
let i = 0;
return () => i < gen1Data.length ? some(gen1Data[i++]) : none;
});

// Source 2 : API paginée (lazy, charge les pages à la demande)
const allPokemon = createIterable(() => {
let page = 0;
let items: Pokemon[] = [];
let index = 0;
return () => {
if (index >= items.length) {
items = fetchPage(page++);
index = 0;
}
return index < items.length ? some(items[index++]) : none;
};
});

// Source 3 : arbre par type (parcours en profondeur)
const byType = createIterable(() => {
const stack = [typeTree];
return () => {
while (stack.length) {
const node = stack.pop()!;
if (node.children) stack.push(...node.children);
if (node.pokemon) return some(node.pokemon);
}
return none;
};
});

// Le code consommateur est identique pour les trois
for (const pokemon of take(10)(gen1)) {
display(pokemon);
}

Une interface, trois sources. Le consommateur ne sait jamais si les données viennent de la mémoire, d'un appel réseau ou d'un parcours d'arbre.


Démo

Parcourez un Pokédex avec trois sources interchangeables : un tableau local Génération 1 (151 Pokémon, fini), une API paginée (lazy, infinie), et un arbre par type. Le code consommateur appelle iterator.next() de façon identique pour les trois sources.

Code
/**
* Iterator factories using Pithos createIterable.
*
* This is the core of the Iterator pattern:
* three different traversal strategies, one interface (Iterable<Pokemon>).
* The consumer calls .next() identically for all three.
*/

import { createIterable } from "@pithos/core/eidos/iterator/iterator";
import { some, none } from "@pithos/core/zygos/option";
import { ALL, EVOLUTION_CHAINS, TYPE_GROUPS } from "./data";
import type { Pokemon, SourceId } from "./types";

// ── Helper: iterate over groups, tagging the first item of each ──────

function createGroupedIterator(
groups: { label: string; items: Pokemon[] }[],
): Iterable<Pokemon> {
return createIterable(() => {
let groupIdx = 0;
let itemIdx = 0;
return () => {
while (groupIdx < groups.length) {
const group = groups[groupIdx];
if (itemIdx < group.items.length) {
const p = group.items[itemIdx];
const result: Pokemon = itemIdx === 0
? { ...p, groupStart: group.label }
: p;
itemIdx++;
return some(result);
}
groupIdx++;
itemIdx = 0;
}
return none;
};
});
}

// ── Three traversal strategies ───────────────────────────────────────

function createByIndexIterator(): Iterable<Pokemon> {
return createIterable(() => {
let i = 0;
return () => i < ALL.length ? some(ALL[i++]) : none;
});
}

function createByEvolutionIterator(): Iterable<Pokemon> {
return createGroupedIterator(
EVOLUTION_CHAINS.map((chain) => ({
label: chain.map((c) => c.name).join(" → "),
items: chain,
})),
);
}

function createByTypeIterator(): Iterable<Pokemon> {
return createGroupedIterator(
TYPE_GROUPS.map((g) => ({
label: `${g.type} (${g.pokemon.length})`,
items: g.pokemon,
})),
);
}

export const ITERATOR_FACTORIES: Record<SourceId, () => Iterable<Pokemon>> = {
byIndex: createByIndexIterator,
byEvolution: createByEvolutionIterator,
byType: createByTypeIterator,
};
Result

Analogie

Une télécommande TV avec les boutons chaîne haut/bas. Vous n'avez pas besoin de savoir comment les chaînes sont stockées en interne. Vous appuyez juste sur « suivant » pour voir la prochaine. La télécommande est l'iterator : elle fournit un accès séquentiel sans exposer les détails internes du décodeur.


Quand l'Utiliser

  • Unifier le parcours de différentes sources de données
  • Traiter des datasets volumineux ou infinis de façon lazy (ne charger que le nécessaire)
  • Chaîner des transformations qui doivent s'exécuter à la demande
  • Masquer les détails d'implémentation de la collection aux consommateurs

Quand NE PAS l'Utiliser

Si toutes vos données tiennent en mémoire et viennent d'un seul tableau, une boucle for...of classique est plus simple. Ne wrappez pas un tableau de 10 éléments dans createIterable juste pour le pattern.


API

  • createIterable — Wrapper un générateur dans un itérable chaînable
  • lazyRange — Créer des plages numériques lazy
  • iterate — Générer des séquences infinies à partir d'une seed et d'une fonction