Powered by AppSignal & Oban Pro

Bases 3/4 — Enum, Stream & compréhensions

Bases/enum_stream_comprehensions.livemd

Retour vers le sommaire des tips Accueil

Bases 3/4 — Enum, Stream & compréhensions

🟢 Niveau débutant · Livebook exécutable (aucune dépendance). Objectif : manipuler des collections comme un Elixirien, et savoir quand préférer un traitement paresseux. Prérequis : Pipe, with et récursion.

Enum : la boîte à outils des collections

Enum regroupe les fonctions qui parcourent une collection immédiatement (eager) et renvoient un résultat.

nombres = 1..10 |> Enum.to_list()

%{
  map: Enum.map(nombres, &(&1 * 2)),
  filter: Enum.filter(nombres, &(rem(&1, 2) == 0)),
  reduce: Enum.reduce(nombres, 0, &+/2),
  sum: Enum.sum(nombres),
  sort: Enum.sort([3, 1, 2], :desc)
}

La notation & (fonctions anonymes courtes)

&(&1 * 2) est un raccourci pour fn x -> x * 2 end. &1 est le premier argument, &2 le second…

# Ces deux écritures sont équivalentes :
double_long = Enum.map(1..3, fn x -> x * 2 end)
double_court = Enum.map(1..3, &(&1 * 2))

double_long == double_court

reduce : le couteau suisse

Presque toutes les fonctions d’Enum peuvent se réécrire avec reduce. C’est l’équivalent fonctionnel d’une boucle avec accumulateur.

# Compter les occurrences de chaque lettre
"mississippi"
|> String.graphemes()
|> Enum.reduce(%{}, fn lettre, acc ->
  Map.update(acc, lettre, 1, &(&1 + 1))
end)

Quelques fonctions à connaître

gens = [
  %{nom: "Ada", ville: "Londres"},
  %{nom: "Alan", ville: "Londres"},
  %{nom: "Grace", ville: "New York"}
]

%{
  group_by: Enum.group_by(gens, & &1.ville, & &1.nom),
  find: Enum.find(gens, &(&1.nom == "Grace")),
  any?: Enum.any?(gens, &(&1.ville == "Paris")),
  count: Enum.count(gens),
  uniq: Enum.uniq([1, 1, 2, 3, 3])
}

Stream : le traitement paresseux (lazy)

Stream a la même API qu’Enum, mais ne calcule rien tant qu’on ne le force pas. Les transformations sont juste « enregistrées », puis exécutées en un seul passage quand un Enum.* final déclenche le calcul.

# Rien n'est calculé ici : on décrit juste un pipeline.
stream =
  1..1_000_000
  |> Stream.map(&(&1 * 3))
  |> Stream.filter(&(rem(&1, 2) == 0))

stream
# Le calcul ne se déclenche qu'ici, et s'arrête dès qu'on a 5 éléments.
stream |> Enum.take(5)

Eager vs Lazy : la différence qui compte

# Avec Enum : DEUX listes intermédiaires d'un million d'éléments sont créées.
eager =
  1..1_000_000
  |> Enum.map(&(&1 + 1))
  |> Enum.filter(&(rem(&1, 5) == 0))
  |> Enum.take(3)

# Avec Stream : un seul passage, on s'arrête après avoir trouvé 3 éléments.
lazy =
  1..1_000_000
  |> Stream.map(&(&1 + 1))
  |> Stream.filter(&(rem(&1, 5) == 0))
  |> Enum.take(3)

{eager, lazy}

Quand choisir Stream ?

  • grandes collections où l’on ne veut pas matérialiser les étapes intermédiaires ;
  • early-exit (take, find) sur une grosse source ;
  • données potentiellement infinies (Stream.iterate, Stream.cycle) ou flux de fichier ligne à ligne (File.stream!).

Pour de petites listes, Enum est parfait (et souvent plus rapide, car Stream a un léger surcoût). Ne « streamez » pas par réflexe.

# Exemple de flux infini, rendu fini par Enum.take
Stream.iterate(1, &(&1 * 2)) |> Enum.take(10)

Les compréhensions for

for est une autre façon de parcourir/filtrer/transformer, pratique surtout pour les filtres et les combinaisons.

# map + filtre en une expression : le `when`-like se fait avec un filtre booléen
for x <- 1..20, rem(x, 3) == 0, do: x * x
# Plusieurs générateurs = produit cartésien
for couleur <- ["rouge", "noir"], taille <- ["S", "M", "L"] do
  "#{couleur}/#{taille}"
end
# `into:` pour construire autre chose qu'une liste (ici une map)
for {cle, val} <- [a: 1, b: 2, c: 3], into: %{} do
  {cle, val * 10}
end

À retenir

  • Enum = traitement immédiat ; couvre l’immense majorité des besoins.
  • &(&1 ...) = fonction anonyme courte ; reduce = la brique universelle.
  • Stream = traitement paresseux : utile pour grandes/infinies sources et early-exit, pas pour les petites listes.
  • for (compréhension) brille pour filtrer et combiner, avec into: pour choisir le conteneur de sortie.

➡️ Suite : Structs, protocoles & behaviours