1. A quoi sert lazy : eager vs « a la demande »
Si vous avez deja prepare un sandwich « pour plus tard », puis l'avez oublie dans le frigo, vous comprenez deja l'idee des evaluations eager. Eager, c'est : « prepare tout a l'avance, au cas ou cela servirait ». lazy, c'est : « je le preparerai quand tu le demanderas vraiment », c'est-a-dire que le calcul n'a lieu qu'au moment ou l'on accede au prochain element. En Swift, c'est particulierement important a cause des chaines map/filter, qui peuvent naivement creer des tableaux intermédiaires et faire plus de travail que necessaire.
Commencons par un exemple contraste : le map classique sur un tableau s'exécute immediatement.
import Foundation
let numbers = [1, 2, 3]
let eager = numbers.map { n -> Int in
print("map:", n) // s'executera immediatement
return n * 2
}
print("eager =", eager) // eager = [2, 4, 6]
Remarquez le comportement : des que nous ecrivons numbers.map { ... }, Swift parcourt tous les elements et execute la fermeture trois fois. Autrement dit, "map:" sera affiche avant le print final.
Maintenant, la meme chose, mais avec lazy. Ici, nous ne « accelerons » pas : nous changeons le moment d'execution.
import Foundation
let numbers = [1, 2, 3]
let lazySeq = numbers.lazy.map { n -> Int in
print("map:", n) // s'executera seulement lors de la consommation
return n * 2
}
print("created") // created
for x in lazySeq {
print("x =", x) // x = 2, x = 4, x = 6
}
Idee importante : lazySeq n'est pas un « tableau pret a l'emploi », mais un « regard paresseux » sur les donnees source. Il conserve un lien (logique) avec la collection de base et la regle de transformation, mais il ne stocke pas les resultats finis.
On peut visualiser cela ainsi :
flowchart LR
A["Donnees source (Array)"] --> B["lazy (enveloppe/vue)"]
B --> C["map/filter (regles)"]
C --> D["Consommation : for-in / reduce / Array(...)"]
Tant qu'il n'y a pas « consommation », les calculs restent comme suspendus dans l'air.
2. Quand lazy aide vraiment
L'erreur la plus frequente chez les debutants consiste a activer lazy « parce que c'est plus rapide, apparemment ». Mais lazy est utile non pas en soi, mais lorsque votre tache permet d'eviter du travail inutile : ne pas parcourir tout le tableau, ne pas creer de tableaux intermédiaires, ne pas calculer ce que vous n'utiliserez de toute facon pas. Dans certaines situations, cela apporte un gain appreciable ; dans d'autres, aucun benefice et une raison supplementaire de se melanger les pinceaux. Dans cette section, nous parlerons donc des scenarios typiques ou la paresse est vraiment justifiee.
Scenario A : on peut s'arreter avant d'atteindre la fin
Classique : vous cherchez le premier element correspondant. Si vous faites une chaine eager et construisez un tableau de resultats, vous avez deja perdu : vous avez tout traite pour n'en prendre qu'un seul.
Exemple (eager — fait plus que necessaire) :
import Foundation
let numbers = Array(1...10)
let doubled = numbers.map { $0 * 2 }
let bigOnes = doubled.filter { $0 >= 12 }
print(bigOnes.first as Any) // Optional(12)
Oui, cela fonctionne. Mais map a parcouru les 10 elements, puis filter a parcouru les 10 elements, alors que nous n'avions besoin que du premier resultat 12.
Exemple (lazy — fait le travail « a la demande ») :
import Foundation
let numbers = Array(1...10)
let firstBig = numbers.lazy
.map { $0 * 2 }
.first { $0 >= 12 }
print(firstBig as Any) // Optional(12)
Ici, le calcul continue jusqu'a ce que first(where:) trouve un element correspondant. Des qu'il l'a trouve, on s'arrete.
Scenario B : la chaine map/filter est longue et les donnees sont volumineuses
Quand la chaine est longue, l'approche eager cree facilement un « millefeuille » de tableaux intermédiaires. Dans les discussions autour de la bibliotheque standard, on souligne justement que l'interet des sequences paresseuses est d'eviter de creer des collections inutiles dans les chaines fonctionnelles.
Imaginez : nous avons une grande liste de chaines, et nous voulons normaliser, filtrer, transformer. Si c'est eager, un nouveau tableau peut apparaitre a chaque etape.
Exemple (pipeline paresseux + consommation limitee) :
import Foundation
let lines = [" Swift ", " ", " Lazy ", "Map", "Filter"]
let cleaned = lines.lazy
.map { $0.trimmingCharacters(in: .whitespaces) }
.filter { !$0.isEmpty }
for word in cleaned {
print(word) // Swift, Lazy, Map, Filter
}
L'idee n'est pas que cela soit « magiquement plus rapide », mais que nous ne creons pas de tableaux separes apres chaque etape : nous faisons passer les elements a travers les regles un par un.
Scenario C : vous travaillez non pas avec Array, mais avec des types pour lesquels le « resultat du filtrage » n'est deja plus un Set/Dictionary
Pour Set, par exemple, on veut souvent filtrer, tout en gardant a l'esprit qu'apres lazy.filter, le resultat n'est deja plus un Set, mais une « enveloppe de collection paresseuse », sans operations de set (comme isSubset(of:)). Dans les materiaux sur Set/Dictionary, cela est presente comme une motivation reelle : parfois, apres lazy.filter, il faut « restituer » le type Set en passant le resultat par Set(...).
import Foundation
let numberSet = Set(1...10)
let lazyFiltered = numberSet.lazy.filter { $0.isMultiple(of: 3) }
// lazyFiltered n'est PAS un Set, mais un resultat paresseux
let backToSet = Set(lazyFiltered)
print(backToSet) // {3, 6, 9} (l'ordre n'est pas garanti)
Ici, lazy peut servir de « mode intermediaire » : on ne se precipite pas pour materialiser le resultat, puis on le materialise exactement la ou c'est vraiment necessaire.
3. Ce qui declenche le calcul dans lazy
lazy a une particularite trompeuse : a premiere vue, le code ressemble a quelque chose de deja calcule, alors qu'en realite nous n'avons fait qu'exprimer des regles. Garder cela a l'esprit aide a comprendre pourquoi le print a l'interieur de lazy.map peut ne pas s'executer « tout de suite », pourquoi le debuggage peut sembler etrange, et pourquoi un meme pipeline paresseux peut effectuer le travail plusieurs fois. Dans cette section, nous construisons un modele pratique : quelles operations sont des « consommateurs » et declenchent le calcul.
Le consommateur le plus simple est for-in : il demande les elements un par un.
import Foundation
let numbers = [1, 2, 3]
let seq = numbers.lazy.map { $0 * 10 }
for x in seq {
print(x) // 10, 20, 30
}
Le second declencheur evident est la materialisation en tableau : Array(seq). Pour construire un tableau, Swift doit « amener » tous les elements jusqu'a la fin (sinon, le tableau ne peut pas etre cree).
import Foundation
let numbers = [1, 2, 3]
let seq = numbers.lazy.map { $0 * 10 }
let arr = Array(seq)
print(arr) // [10, 20, 30]
Ensuite viennent des consommateurs qui peuvent etre « courts-circuites » (short-circuit). Par exemple, contains(where:) peut s'arreter plus tot si la condition est remplie.
import Foundation
let numbers = [1, 2, 3, 4, 5]
let hasBig = numbers.lazy
.map { $0 * 2 }
.contains { $0 >= 8 }
print(hasBig) // true
Et c'est la que lazy est particulierement agreable : si la condition a ete satisfaite au quatrieme element, le cinquieme peut ne jamais etre calcule.
4. Quand lazy gene
Parfois, lazy ressemble a une personne tres economique qui eteint la lumiere dans le couloir tant que vous n'etes pas encore arrive a la cuisine : il y a un gain, mais cela agace. Le probleme ne vient pas de lazy, mais d'attentes incorrectes. Les debutants pensent souvent que lazy « calcule une fois et memorise » — mais ce n'est pas un cache, c'est une « recette ». Si vous parcourez deux fois la sequence paresseuse, vous preparez le plat deux fois. Et si la fermeture contient des effets de bord (par exemple, print), vous les obtiendrez soudain de nouveau.
Piege A : lazy ne met pas le resultat en cache
import Foundation
let numbers = [1, 2, 3]
let seq = numbers.lazy.map { n -> Int in
print("calc", n)
return n * 2
}
for x in seq { print("first", x) }
for x in seq { print("second", x) }
La sortie sera la suivante (schematiquement) :
calc 1
first 2
calc 2
first 4
calc 3
first 6
calc 1
second 2
calc 2
second 4
calc 3
second 6
Autrement dit, le calcul a ete repete. Si vous avez besoin d'un resultat « calcule une fois puis reutilise plusieurs fois », vous devez en general materialiser : let arr = Array(seq).
Piege B : « types enormes » et non-evidence des chaines
Du point de vue de la lecture du code, lazy a un cout : les chaines peuvent devenir moins evidentes, et dans les discussions reelles de la bibliotheque standard on mentionnait aussi un point technique — de longues chaines lazy.map/filter/map/filter peuvent conduire a des enveloppes lourdes et a un code pas tres agreable que le compilateur devra ensuite optimiser.
Nous n'entrerons pas dans les « entrailles du compilateur », mais la conclusion pratique est simple : il ne faut pas faire des « draps » lazy de 12 methodes en une seule ligne. Si la chaine devient illisible, mieux vaut simplifier : soit la couper en variables, soit utiliser une boucle si elle est plus claire.
Piege C : lazy ne rend pas les petites collections « plus rapides par definition »
Si vous avez un tableau de 10 elements, et qu'a l'interieur de map l'operation * 2 est simple, il n'y aura presque aucun benefice reel. En revanche, il y aura une abstraction supplementaire dans votre tete : quand est-ce execute ? pourquoi pas maintenant ? pourquoi ne vois-je pas le resultat ? Pour un debutant, cela gene plus que cela n'aide.
5. Regles simples de choix
On aimerait avoir une « checklist » a garder en tete lorsque la main se tend vers .lazy simplement parce que le mot est court et a l'air professionnel. Mais la realite est la suivante : la regle depend de l'objectif — economiser la memoire, economiser le CPU, vouloir une sortie precoce, vouloir de la lisibilite. C'est pourquoi je donne ci-dessous un tableau, puis j'en expliquerai le sens avec des mots humains, pour que cela ressemble moins a des « commandements » qu'a une logique de choix compréhensible.
| Situation dans la tache | Ce qui est souvent preferable | Pourquoi |
|---|---|---|
| On n'a besoin que du premier/de quelques premiers resultats | lazy + consommateur court (first, contains, prefix) | On peut s'arreter plus tot et ne pas traiter la fin |
| Les donnees sont volumineuses, la chaine de transformations est longue | lazy aux etapes intermédiaires | Moins de tableaux intermédiaires |
| Le resultat est necessaire plusieurs fois (plusieurs passages) | Materialiser (Array(...)) | lazy ne met pas en cache, sinon il y aura du travail repete |
| La collection est petite et l'operation est peu couteuse | map/filter classiques | La lisibilite compte davantage, la difference de vitesse est presque nulle |
| Il y a print, journalisation ou compteurs dans les fermetures | Rester prudent avec lazy | Les effets de bord sont retardes et repetes |
| On a besoin de l'API d'un type precis (operations de Set, methodes de dictionnaire) | Materialiser a nouveau (Set(...), Array(...)) | Apres lazy.filter, le type devient en general une « enveloppe » |
| La chaine est devenue une « nappe » | La couper en variables ou passer a une boucle | Le cout de lazy se trouve dans la lisibilite et le debuggage |
Maintenant, quelques explications importantes.
Quand vous voulez « prendre seulement quelques premiers », lazy est presque ideal : vous avez decrit les regles, puis un consommateur comme first(where:) s'arretera de lui-meme. C'est l'un des cas les plus agreables, car la performance gagne, et le code reste court.
Quand vous voulez « produire un resultat une fois et l'utiliser plusieurs fois », lazy ne convient generalement pas tel quel. Il ne met pas en cache, donc un nouveau passage repete aussi le calcul. Dans ces cas-la, il est plus logique de construire une chaine paresseuse, puis de la materialiser une fois dans un tableau, et d'utiliser ensuite ce tableau.
Quand la collection est petite, lazy devient souvent un « bruit optique ». C'est comme mettre un casque pour aller du canape au frigo : en theorie c'est plus sur, mais cela a l'air bizarre, et vous passerez plus de temps a enfiler le casque.
Quand vous avez un Set ou un Dictionary, il est utile de se rappeler qu'apres des operations paresseuses, vous obtenez souvent non pas « le meme type », mais un resultat paresseux special. C'est normal, mais vous perdez parfois des methodes pratiques du type (pour Set — les operations sur les ensembles). Dans ce cas, rematerialiser via Set(...) n'est pas une « rustine », mais une etape normale.
6. Exemple : lazy dans une application pedagogique
Jusqu'a present, nous avons regarde lazy dans le vide, comme une fonctionnalite isolee. Mais c'est lorsqu'on l'utilise « pour de vrai » dans une petite application qu'on la retient le mieux : par exemple, nous stockons une liste de livres (des chaines) et voulons trouver rapidement les premiers correspondant a la requete de l'utilisateur. Nous ne construisons pas encore une vraie architecture et n'introduisons pas struct (ce sera plus tard), donc nous utilisons des tableaux, des chaines et des fonctions simples.
Supposons que nous ayons un catalogue — simplement un tableau de chaines. L'utilisateur saisit une sous-chaine a rechercher, et nous voulons afficher au maximum 3 premieres correspondances. Point important : « au maximum 3 » signifie qu'il n'est pas necessaire de parcourir tout le catalogue si nous avons deja assez de correspondances.
import Foundation
let books = [
"Clean Code",
"Swift Programming",
"The Pragmatic Programmer",
"Swift Algorithms",
"Code Complete"
]
let query = "swift"
let matches = books.lazy
.filter { $0.lowercased().contains(query) }
.prefix(3)
for title in matches {
print(title) // Swift Programming, Swift Algorithms
}
Ici, lazy n'est pas un ornement. Il permet de transformer la tache en « flux » : nous lisons les livres un par un, nous verifions la condition, et des que prefix(3) a reunis trois elements (ou que les livres sont epuises), tout s'arrete. Nous ne construisons pas un tableau de toutes les correspondances s'il n'est pas necessaire.
Si vous voulez voir clairement la difference entre « paresseux » et « immediat », vous pouvez comparer avec la version eager. Elle est elle aussi correcte, mais elle fait du travail inutile si le catalogue est grand.
Exemple (eager) :
import Foundation
let books = ["A", "Swift B", "Swift C", "D", "Swift E"]
let query = "swift"
let allMatches = books
.filter { $0.lowercased().contains(query) }
let firstThree = Array(allMatches.prefix(3))
print(firstThree) // ["Swift B", "Swift C", "Swift E"]
Ici, nous filtrons quand meme tout le tableau, meme si nous n'avons besoin que des trois premiers.
Compliquons un peu et ajoutons une « note » sous forme de dictionnaire. Nous voulons afficher les livres ayant une note >= 8, mais pas plus de deux lignes, pour ne pas encombrer la console. Notez que l'ordre d'un dictionnaire n'est pas garanti — c'est normal, nous ne comptons pas dessus.
import Foundation
let ratings: [String: Int] = [
"Clean Code": 9,
"Swift Programming": 7,
"Code Complete": 10,
"Some Book": 5
]
let top = ratings.lazy
.filter { (_, score) in score >= 8 }
.prefix(2)
for (title, score) in top {
print("\(title): \(score)")
}
Encore une fois, lazy est utile ici precisement parce que nous savons d'avance : « deux me suffiront ». Si nous voulions trier par note, ce serait une autre histoire (et nous parlerions d'autres outils), mais dans le cadre de cette journee, nous restons concentres sur le parcours et la consommation.
7. Erreurs typiques lors de l'utilisation de lazy
Erreur n°1 : s'attendre a ce que lazy “calcule et memorise”.
C'est le piege mental le plus courant : la personne percoit lazy comme un cache. En realite, lazy est une « vue + des regles », et a chaque nouveau passage le calcul sera refait. Si le resultat est necessaire plusieurs fois, il vaut en general mieux le materialiser une fois dans un Array(...) et travailler ensuite avec le tableau.
Erreur n°2 : ajouter lazy “au cas ou” sur de petites collections.
Sur des petits tableaux avec des transformations simples, lazy apporte rarement un gain appreciable, mais ajoute presque toujours une charge cognitive : « quand cela va-t-il s'executer ? », « pourquoi rien ne se passe maintenant ? ». Dans les exercices pedagogiques, cela gene encore plus, parce que le debuggage devient moins direct.
Erreur n°3 : s'etonner que print dans lazy.map s'active “tard” ou “plusieurs fois”.
Les effets de bord sont retardes jusqu'a la consommation. Et si vous consommez la sequence deux fois (par exemple, deux boucles for-in), les effets se repetent. C'est pourquoi lazy et les effets de bord sont de mauvais amis : mieux vaut garder les fermetures pures, sans print ni compteurs, ou alors comprendre tres precisement quand la consommation aura lieu.
Erreur n°4 : perdre les methodes du type d'origine et essayer de les appeler sur le resultat paresseux.
Apres numberSet.lazy.filter { ... }, vous obtenez non pas un Set, mais une enveloppe paresseuse. C'est un comportement normal : lazy vous donne une sequence a parcourir, mais pas « toutes les possibilites du Set ». Si vous avez besoin des operations d'ensemble, rematerialisez via Set(...).
Erreur n°5 : transformer la chaine en « nappe » illisible et esperer que lazy resoudra tout.
lazy n'est pas un anti-pattern, mais ce n'est pas non plus une pilule universelle. Si l'expression devient trop longue, mieux vaut separer les etapes en variables intermediaires ou meme revenir a une boucle classique. Sinon, vous gagnez quelques microsecondes (au mieux), mais vous perdez des heures en lecture et en debuggage.
GO TO FULL VERSION