Powered by AppSignal & Oban Pro

Bases 1/4 — Immutabilité & pattern matching

immutabilite_et_pattern_matching.livemd

Retour vers le sommaire des tips Accueil

Bases 1/4 — Immutabilité & pattern matching

🟢 Niveau débutant · Livebook exécutable (aucune dépendance). Objectif : comprendre les deux idées qui déroutent le plus quand on vient de Python, JS, PHP ou Java. Prérequis : savoir lire un peu de code.

Idée n°1 : les données sont immuables

En Elixir, on ne modifie jamais une donnée existante. On en crée une nouvelle à partir de l’ancienne.

liste = [1, 2, 3]
autre = [0 | liste]

# `liste` n'a pas changé : on a fabriqué une nouvelle liste.
{liste, autre}

Même chose pour les maps : les fonctions renvoient une copie modifiée, l’original reste intact.

user = %{nom: "Ada", age: 36}
plus_vieille = Map.put(user, :age, 37)

{user, plus_vieille}

Pourquoi c’est une bonne nouvelle :

  • aucun « effet de bord à distance » (une fonction ne peut pas saboter vos données dans votre dos) ;
  • la concurrence devient sûre : deux processus ne peuvent pas corrompre une donnée partagée.

⚠️ Conséquence : Map.put(user, ...) ne suffit pas, il faut récupérer le résultat. Oublier d’affecter le retour est l’erreur n°1 du débutant.

Idée n°2 : pas de boucle for classique

Il n’y a pas de i++ ni de variable de boucle qu’on incrémente (puisque rien n’est mutable). On parcourt les collections avec des fonctions (Enum.map, etc.) ou avec la récursion. On verra ça en détail dans les notebooks suivants.

# Au lieu d'une boucle qui modifie un accumulateur :
Enum.map([1, 2, 3], fn x -> x * 10 end)

Idée n°3 : = n’est PAS une affectation

= est l’opérateur de correspondance (match). À gauche un motif, à droite une valeur. Elixir essaie de faire correspondre les deux.

x = 1
# Ici Elixir lie x à 1 car c'est la seule façon de faire correspondre les deux côtés.
x

La preuve que ce n’est pas une simple affectation :

# 1 = x fonctionne car x vaut déjà 1 : les deux côtés correspondent.
1 = x
# Mais ceci échoue (MatchError) car 2 ne correspond pas à la valeur de x (1).
# Décommentez pour voir l'erreur :
# 2 = x

Déstructurer avec le pattern matching

Le vrai pouvoir : extraire des valeurs en décrivant la forme attendue.

# Tuple
{:ok, valeur} = {:ok, 42}
valeur
# Liste : tête | reste
[premier | reste] = [10, 20, 30]
{premier, reste}
# Map : on n'extrait que les clés qui nous intéressent
%{nom: nom} = %{nom: "Ada", age: 36}
nom

C’est LE pattern qu’on retrouve partout en Elixir, notamment pour gérer les retours {:ok, ...} / {:error, ...} :

resultat = {:error, :introuvable}

case resultat do
  {:ok, valeur} -> "Trouvé : #{inspect(valeur)}"
  {:error, raison} -> "Échec : #{inspect(raison)}"
end

Le pin operator ^

Par défaut, une variable à gauche du = est (re)liée. Pour dire « utilise la valeur actuelle de la variable comme motif » plutôt que de la réaffecter, on l’épingle avec ^.

attendu = 1

# Sans ^, ceci réaffecterait `attendu`. Avec ^, on EXIGE que la droite vaille 1.
^attendu = 1
"ok, la valeur correspond bien"

C’est ce même ^ qu’on utilise dans les requêtes Ecto (where: u.id == ^id).

Pattern matching dans les fonctions

On peut écrire plusieurs clauses d’une fonction, chacune avec un motif différent. Elixir choisit la première qui correspond — souvent plus lisible qu’une cascade de if.

defmodule Salutation do
  def bonjour(:fr), do: "Bonjour !"
  def bonjour(:en), do: "Hello!"
  def bonjour(:es), do: "¡Hola!"
  # clause « attrape-tout »
  def bonjour(_autre), do: "👋"
end

Enum.map([:fr, :en, :es, :it], &Salutation.bonjour/1)

À retenir

  • Les données sont immuables : on crée du neuf, on n’altère jamais l’ancien → récupérez toujours le retour.
  • Pas de boucle mutable : on utilise des fonctions de collection ou la récursion.
  • = fait du pattern matching, pas de l’affectation.
  • La déstructuration extrait des valeurs en décrivant la forme attendue.
  • ^ épingle la valeur courante au lieu de réaffecter.
  • Les clauses de fonction remplacent élégamment beaucoup de if/else.

➡️ Suite : Pipe, with et récursion