Traverse¶
Le module fptk.adt.traverse propose des opérations pour manipuler des collections de valeurs Option ou Result, permettant de « retourner » la structure du conteneur tout en assurant une gestion des erreurs robuste.
Concept : Traverse et Sequence¶
Face à une liste de calculs susceptibles d'échouer, deux besoins majeurs apparaissent :
- Sequence : transformer une
list[Option[T]]enOption[list[T]]. - Traverse : appliquer une fonction à une liste d'éléments simples, puis regrouper les résultats faillibles en un seul conteneur.
Ces opérations inversent littéralement l'imbrication des conteneurs :
C'est une approche indispensable pour :
- Une sémantique d'arrêt immédiat (fail-fast) : stopper le traitement au premier
NothingouErrrencontré. - Des résultats « tout ou rien » : soit vous obtenez la liste complète des succès, soit vous recevez le signal du premier échec.
- Des pipelines fluides : manipulez des collections d'opérations faillibles comme s'il s'agissait d'une opération unique.
Le problème : l'accumulation manuelle laborieuse¶
def recuperer_tous_utilisateurs(ids: list[int]) -> list[User]:
resultats = []
for id in ids:
utilisateur = fetch_user(id) # Renvoie Option[User]
if utilisateur.is_none():
return [] # Que faire si un seul échoue ? Tout abandonner ?
resultats.append(utilisateur.unwrap())
return resultats
# Ce code est verbeux, propice aux erreurs et peu lisible.
La solution avec Traverse¶
from fptk.adt.traverse import traverse_option
def recuperer_tous_utilisateurs(ids: list[int]) -> Option[list[User]]:
return traverse_option(ids, fetch_user)
# Renvoie Some([users...]) si TOUS ont réussi.
# Renvoie NOTHING si au moins UN a échoué.
Le code devient concis, sa sémantique est explicite et il s'intègre parfaitement au reste de l'écosystème fptk.
API¶
Fonctions Sequence¶
| Fonction | Signature | Description |
|---|---|---|
sequence_option(xs) |
Iterable[Option[A]] -> Option[list[A]] |
Collecte les valeurs Some si elles sont toutes présentes. |
sequence_result(xs) |
Iterable[Result[A, E]] -> Result[list[A], E] |
Collecte les valeurs Ok si elles ont toutes réussi. |
Fonctions Traverse¶
| Fonction | Signature | Description |
|---|---|---|
traverse_option(xs, f) |
(Iterable[A], A -> Option[B]) -> Option[list[B]] |
Applique f et collecte les succès. |
traverse_result(xs, f) |
(Iterable[A], A -> Result[B, E]) -> Result[list[B], E] |
Applique f et collecte les succès. |
Variantes asynchrones (Async)¶
| Fonction | Mode d'exécution | Description |
|---|---|---|
traverse_*_async |
Séquentiel | Applique et collecte un par un. |
traverse_*_parallel |
Parallèle | Applique et collecte tout de front. |
Quand choisir quelle variante ?
- Séquentiel (
*_async) : idéal pour les API avec limitation de débit (rate limiting) ou les opérations interdépendantes. - Parallèle (
*_parallel) : à privilégier pour les tâches indépendantes afin d'obtenir un débit maximal.
Fonctionnement technique¶
Comportement Fail-Fast¶
Toutes ces opérations adoptent une stratégie d'arrêt immédiat (fail-fast). Dès qu'un échec survient :
- Le traitement s'interrompt (économie de ressources).
- Seule la première erreur rencontrée est renvoyée.
- Pour collecter toutes les erreurs, utilisez plutôt
validate_all.
Exemples d'utilisation¶
Analyse d'une liste de saisies¶
from fptk.adt.traverse import traverse_option
from fptk.adt.option import Some, NOTHING
def analyser_entier(s: str) -> Option[int]:
try:
return Some(int(s))
except ValueError:
return NOTHING
# Analyse tout... ou rien
resultat = traverse_option(["1", "2", "3"], analyser_entier)
# Some([1, 2, 3])
resultat = traverse_option(["1", "erreur", "3"], analyser_entier)
# NOTHING (le traitement s'est arrêté à "erreur")
Parcours asynchrone¶
async def recuperer_users_parallele(ids: list[int]) -> Result[list[User], str]:
# Déclenche toutes les requêtes simultanément
return await traverse_result_parallel(ids, fetch_user_async)
async def recuperer_users_sequentiel(ids: list[int]) -> Result[list[User], str]:
# Interroge la base un par un (plus prudent pour les gros volumes)
return await traverse_result_async(ids, fetch_user_async)
Traverse vs validate_all : le match¶
| Opération | Stratégie | Usage recommandé |
|---|---|---|
traverse_result |
Arrêt immédiat (fail-fast). | Logique interne, pipelines techniques. |
validate_all |
Accumulation complète. | Saisie utilisateur, formulaires web. |
Quand utiliser Traverse ?¶
Privilégiez Traverse lorsque :
- Vous traitez une collection de données de façon uniforme.
- Chaque étape du traitement est susceptible d'échouer.
- Vous exigez que la totalité de la collection soit valide pour continuer.
- Seule la première erreur survenue vous intéresse.
Privilégiez *_parallel lorsque :
- Les tâches sont totalement indépendantes les unes des autres.
- Vous visez la meilleure performance brute possible.
Voir aussi¶
Option— Le type de base pour les valeurs facultatives.Result— Le type de base pour les calculs faillibles.validate_all— Pour accumuler l'ensemble des erreurs de validation.gather_results— Pour orchestrer des opérations asynchrones parallèles.