# AP2 — Défis ★

## Avant de commencer

Ce notebook est facultatif. Commencez par le tronc commun du cycle AP1–AP3,
puis choisissez un défi si vous souhaitez approfondir. Une recherche expliquée
et testée peut être jointe au rendu du 6 décembre 2026 pour un bonus ; elle
n'est pas nécessaire pour valider le minimum demandé.

Ces défis sont volontairement raides, avec des énoncés brefs et peu d'indices :
rejouer une recette du cours ne vous apprendrait rien de neuf, et nous vous
demandons ici de chercher. Prenez-les dans l'ordre qui
vous chante, lâchez celui qui vous lasse, revenez-y. Le dernier est taillé
pour tenir tête à un très bon élève pendant une bonne heure, à finir chez soi
au besoin.

Si vous séchez une heure, n'y voyez pas un échec : c'est plutôt le signe que
ça travaille. Les vérifications testent des **propriétés** sur plusieurs cas,
dont des cas limites, plutôt qu'une valeur unique. Recopier une réponse ne
marchera donc pas : il faut que le raisonnement tienne.

Contrainte de jeu : entraînez ici les collections et les motifs —
listes, tuples, maps, keyword lists, le pattern matching (destructuration,
pin `^`, `_`), et le pipe `|>`. Les fonctions d'`Enum` (`map`, `filter`,
`sum`, `sort`, `uniq`, `count`…) et de `String` qui seront approfondies en AP4 sont
autorisées ; utilisez leurs exemples au besoin. Pas de `Enum.reduce`, pas de récursion, pas de module à vous :
ce sont des contraintes volontaires pour chercher des compositions de fonctions.

## Défi 1 — Aplatir l'imbriqué (réécriture extrême au pipe)

Voici une expression **imbriquée sur cinq niveaux**. Elle est correcte, mais
illisible : pour la comprendre, il faut la lire de l'intérieur vers
l'extérieur, à l'envers de son exécution.

```elixir
Enum.sum(
  Enum.map(
    Enum.filter(
      Enum.map(
        String.split("7 2 9 4 11 6 3 8"),
        fn s -> String.to_integer(s) end
      ),
      fn n -> rem(n, 2) == 1 end
    ),
    fn n -> n * n end
  )
)
```

Réécrivez-la **à l'identique** en un seul pipeline `|>`, lié à `resultat`.
Cinq étapes, lues de haut en bas, dans l'ordre où elles s'exécutent. Aucune
variable intermédiaire.

```elixir
# À COMPLÉTER : le pipeline équivalent, lié à `resultat`.
resultat =
  "7 2 9 4 11 6 3 8"

resultat
```

Vérification :

```elixir
attendu =
  Enum.sum(
    Enum.map(
      Enum.filter(
        Enum.map(String.split("7 2 9 4 11 6 3 8"), fn s -> String.to_integer(s) end),
        fn n -> rem(n, 2) == 1 end
      ),
      fn n -> n * n end
    )
  )

if resultat != attendu do
  raise """
  Votre pipeline donne #{inspect(resultat)}, l'imbriqué donne #{attendu}.
  Ordre attendu : split, puis map (vers entiers), filter (impairs), map (carrés), sum.
  Une étape mal placée, et le résultat change. Déroulez l'imbriqué de l'intérieur.
  """
end

# propriété : le pipeline doit aussi marcher sur une AUTRE entrée, même règle.
# (on reconstruit la même chaîne de traitement à part et on compare l'idée)
autre = "1 1 1 2 2"
via_ref =
  autre
  |> String.split()
  |> Enum.map(&String.to_integer/1)
  |> Enum.filter(fn n -> rem(n, 2) == 1 end)
  |> Enum.map(fn n -> n * n end)
  |> Enum.sum()

if via_ref != 3 do
  raise "vérification de référence incohérente (#{via_ref}) — signalez-le à l'enseignant"
end

IO.puts("✅ Cinq niveaux d'imbrication devenus un pipeline lisible — resultat = #{resultat}")
```

> En une phrase : le pipe est « juste » du réarrangement syntaxique
> (`a |> f(b)` devient `f(a, b)`). Pourquoi cela impose-t-il que la donnée
> transformée soit toujours le **premier** argument de la fonction à droite ?
>
> Votre réponse : …

## Défi 2 — Extraction chirurgicale (un seul motif)

On vous donne une valeur profondément imbriquée : une réponse d'API
simulée, mêlant tuples, maps et listes.

```elixir
reponse =
  {:ok,
   %{
     statut: 200,
     corps: %{
       utilisateur: %{nom: "Lovelace", roles: [:admin, :editeur]},
       messages: [%{id: 1, texte: "salut"}, %{id: 2, texte: "coucou"}]
     }
   }}
```

En **un seul** `=` (un seul motif à gauche, aussi imbriqué qu'il le faut),
liez **d'un coup** :

* `nom`, qui vaudra `"Lovelace"` ;
* `premier_role`, qui vaudra `:admin` (la **tête** de la liste des rôles) ;
* `premier_texte`, qui vaudra `"salut"` (le champ `texte` du **premier** message).

Le motif doit en plus **exiger** que le statut soit `200` et que l'étiquette
de tête soit `:ok` — sans lier ces deux-là (ce sont des valeurs imposées, pas
des captures).

```elixir
reponse =
  {:ok,
   %{
     statut: 200,
     corps: %{
       utilisateur: %{nom: "Lovelace", roles: [:admin, :editeur]},
       messages: [%{id: 1, texte: "salut"}, %{id: 2, texte: "coucou"}]
     }
   }}

# À COMPLÉTER : UN seul motif à gauche du = qui capture nom, premier_role,
# premier_texte, et impose :ok et statut: 200.
# {:ok, %{...}} = reponse

{nom, premier_role, premier_texte}
```

Vérification :

```elixir
cond do
  nom != "Lovelace" ->
    raise "nom devrait valoir \"Lovelace\", obtenu #{inspect(nom)}"

  premier_role != :admin ->
    raise "premier_role devrait valoir :admin (la TÊTE de [:admin, :editeur]), obtenu #{inspect(premier_role)}"

  premier_texte != "salut" ->
    raise "premier_texte devrait valoir \"salut\" (texte du 1er message), obtenu #{inspect(premier_texte)}"

  true ->
    :ok
end

# propriété : le motif doit REFUSER une réponse au statut différent de 200.
# (si votre motif capture le statut au lieu de l'imposer, ce test le révèle)
mauvais =
  {:ok,
   %{
     statut: 500,
     corps: %{
       utilisateur: %{nom: "X", roles: [:invite]},
       messages: [%{id: 9, texte: "erreur"}]
     }
   }}

# Le motif attendu (celui que vous deviez écrire) impose statut: 200. On montre
# ici qu'un tel motif refuse une réponse à statut 500 : c'est la propriété que
# votre motif doit posséder. Vérifiez à la main que le vôtre la possède aussi.
motif_refuse_500 =
  try do
    {:ok,
     %{
       statut: 200,
       corps: %{
         utilisateur: %{nom: _n, roles: [_r | _]},
         messages: [%{texte: _t} | _]
       }
     }} = mauvais

    false
  rescue
    MatchError -> true
  end

if not motif_refuse_500 do
  raise "un motif qui impose statut: 200 doit refuser une réponse à statut 500"
end

IO.puts("✅ Un seul motif, trois captures profondes, et le statut 200 bien exigé")
```

> En une phrase : sur une **map**, un motif ne mentionne que les clés qui
> l'intéressent et ignore le reste, alors que sur un **tuple** ou une
> **liste** la forme doit correspondre. En quoi cette asymétrie a-t-elle
> guidé l'écriture de votre motif ?
>
> Votre réponse : …

## Défi 3 — L'annuaire qu'on ne mute jamais

`annuaire` associe des prénoms à des fiches (une map par personne). On veut
appliquer **quatre** changements, en produisant à chaque fois une **nouvelle**
map, sans jamais toucher l'originale — une chaîne de transformations.

Partez de `annuaire` et enchaînez, **dans cet ordre**, pour obtenir `final` :

1. Ada **déménage** : sa `ville` passe de `"Londres"` à `"Paris"` (la clé
   existe déjà : syntaxe `%{... | ...}`).
2. Ada gagne un **email** `"ada@paris.fr"` (clé absente : `Map.put/3`).
3. On **ajoute** une personne, Grace : `%{ville: "Baltimore"}`.
4. On **retire** Alan de l'annuaire (cherchez dans la doc de `Map` la
   fonction qui retire une clé).

`annuaire` doit rester **intacte** à la fin.

```elixir
annuaire = %{
  ada: %{ville: "Londres"},
  alan: %{ville: "Manchester"}
}

# À COMPLÉTER : construis `final` en enchaînant les 4 transformations.
# Astuce : pour la fiche d'Ada, vous devez d'abord la sortir, la transformer,
# puis la remettre : une map imbriquée ne se met pas à jour « en profondeur »
# d'un seul geste avec ce que vous connaissez.
final = annuaire

{annuaire, final}
```

Vérification :

```elixir
# 1) l'original n'a pas bougé
if annuaire != %{ada: %{ville: "Londres"}, alan: %{ville: "Manchester"}} do
  raise "annuaire a été modifié (#{inspect(annuaire)}) — or on ne mute jamais : on fabrique du neuf"
end

# 2) le résultat, champ par champ (propriétés, pas comparaison globale opaque)
if not is_map(final) do
  raise "final doit être une map"
end

if Map.has_key?(final, :alan) do
  raise "Alan devrait avoir été retiré de `final` (cherche Map.delete/2)"
end

if not Map.has_key?(final, :grace) or final.grace != %{ville: "Baltimore"} do
  raise "Grace devrait être présente avec %{ville: \"Baltimore\"}, obtenu #{inspect(Map.get(final, :grace))}"
end

fiche_ada = Map.get(final, :ada)

cond do
  fiche_ada == nil ->
    raise "Ada a disparu de `final`"

  Map.get(fiche_ada, :ville) != "Paris" ->
    raise "la ville d'Ada devrait être \"Paris\", obtenu #{inspect(Map.get(fiche_ada, :ville))}"

  Map.get(fiche_ada, :email) != "ada@paris.fr" ->
    raise "l'email d'Ada devrait être \"ada@paris.fr\", obtenu #{inspect(Map.get(fiche_ada, :email))}"

  true ->
    :ok
end

# 3) Ada ne doit avoir QUE ville et email (pas de clé parasite)
if Map.keys(fiche_ada) |> Enum.sort() != [:email, :ville] do
  raise "la fiche d'Ada devrait avoir exactement les clés :email et :ville, obtenu #{inspect(Map.keys(fiche_ada))}"
end

IO.puts("✅ Quatre transformations enchaînées, annuaire d'origine intact")
```

> En une phrase : vous vouliez « changer la ville d'Ada », mais Ada est une
> map **dans** une map. Pourquoi n'avez-vous pas pu le faire d'un seul
> `%{annuaire | ...}`, et qu'a-t-il fallu faire à la place ?
>
> Votre réponse : …

## Défi 4 — Le validateur qui épingle

Le pin `^` compare au lieu de lier. On s'en sert ici pour écrire un
**validateur d'accréditation** : une entrée n'est acceptée **que si** son
étiquette et sa zone coïncident *exactement* avec des valeurs attendues,
fixées à l'avance. Le code, lui, est capturé.

On vous donne `etiquette_ok = :badge` et `zone_ok = :labo`. Pour chacune des
trois entrées ci-dessous, dites si elle est acceptée en tentant un
**match avec pins** ; produisez la liste `verdicts` des trois résultats
(`{:accepte, code}` si ça matche, `:refuse` sinon).

Pour ce défi, utilisez un
`try`/`rescue` autour de chaque match, ou un `case` avec un motif épinglé.

```elixir
etiquette_ok = :badge
zone_ok = :labo

entrees = [
  {:badge, :labo, 4021},
  {:badge, :accueil, 7},
  {:carte, :labo, 4021}
]

# À COMPLÉTER : pour chaque entrée, un match {^etiquette_ok, ^zone_ok, code}.
# Construis `verdicts` = liste de {:accepte, code} ou :refuse, dans l'ordre.
verdicts = nil

verdicts
```

Vérification :

```elixir
attendu = [{:accepte, 4021}, :refuse, :refuse]

if verdicts != attendu do
  raise """
  Attendu #{inspect(attendu)}, obtenu #{inspect(verdicts)}.
  Rappel : ^etiquette_ok EXIGE :badge, ^zone_ok EXIGE :labo. La 2e entrée
  a la bonne étiquette mais la mauvaise zone ; la 3e a la mauvaise étiquette.
  Seule la 1re satisfait les DEUX pins — et capture 4021.
  """
end

# propriété : si on change les valeurs attendues, le verdict doit suivre.
# On rejoue la logique de référence avec d'autres attentes pour s'assurer
# que vous avez bien épinglé des variables, sans coder :badge/:labo en dur.
verdict = fn et, zn, {e, z, c} ->
  if e == et and z == zn, do: {:accepte, c}, else: :refuse
end

controle = [
  verdict.(:carte, :labo, {:carte, :labo, 9}),
  verdict.(:badge, :labo, {:carte, :labo, 9})
]

if controle != [{:accepte, 9}, :refuse] do
  raise "logique de référence incohérente — signalez-le à l'enseignant"
end

IO.puts("✅ Le validateur n'accepte que la paire (:badge, :labo) et capture son code")
```

> En une phrase : qu'aurait produit le motif `{etiquette, zone, code}`
> **sans** les `^` sur la première entrée — et pourquoi aurait-il alors
> accepté **n'importe quelle** étiquette et n'importe quelle zone ?
>
> Votre réponse : …

## Défi 5 — Le rapport (le gros morceau)

On vous confie un journal d'événements bruts : une liste de tuples de formes
différentes. Vous devez en extraire un **rapport**, en un seul pipeline, sans
écrire une seule fonction nommée.

```elixir
journal = [
  {:achat, %{client: "ada", montant: 30, ville: "paris"}},
  {:connexion, %{client: "alan"}},
  {:achat, %{client: "grace", montant: 50, ville: "paris"}},
  {:error, "timeout"},
  {:achat, %{client: "ada", montant: 20, ville: "lyon"}},
  {:achat, %{client: "ada", montant: 15, ville: "paris"}},
  {:connexion, %{client: "ada"}}
]
```

Produisez `total_paris` : la **somme des montants** de tous les événements
`:achat` **dont la ville est `"paris"`**. Les autres formes d'événements
(`:connexion`, `:error`) doivent être **ignorées sans faire planter** le
pipeline — c'est le cœur de la difficulté : votre chaîne doit trier les
formes *avant* de toucher aux champs qui n'existent que sur les achats.

Squelette suggéré (à vous d'en décider autrement) :

```elixir
journal = [
  {:achat, %{client: "ada", montant: 30, ville: "paris"}},
  {:connexion, %{client: "alan"}},
  {:achat, %{client: "grace", montant: 50, ville: "paris"}},
  {:error, "timeout"},
  {:achat, %{client: "ada", montant: 20, ville: "lyon"}},
  {:achat, %{client: "ada", montant: 15, ville: "paris"}},
  {:connexion, %{client: "ada"}}
]

# À COMPLÉTER (1) : ne garder que les achats à Paris.
#   Piste : Enum.filter avec un match?/2 sur la forme {:achat, %{ville: "paris"}},
#   ou un filtre qui teste la forme du tuple. Un :connexion ne doit PAS planter.
# À COMPLÉTER (2) : extraire le montant de chacun (Enum.map + motif).
# À COMPLÉTER (3) : sommer (Enum.sum).
total_paris = nil

total_paris
```

Vérification :

```elixir
if total_paris != 95 do
  raise """
  total_paris devrait valoir 95 (30 + 50 + 15, les trois achats à Paris),
  obtenu #{inspect(total_paris)}.
  Pièges classiques :
  - avez-vous compté l'achat à Lyon (20) ? il ne doit pas y être.
  - un :connexion ou :error a-t-il fait planter le pipeline ? filtre la FORME
    ({:achat, %{ville: "paris"}}) AVANT de lire le champ :montant.
  """
end

# propriété d'invariance : ajouter un événement NON pertinent ne change rien.
journal_bruite =
  journal ++
    [{:connexion, %{client: "zoe"}}, {:achat, %{client: "zoe", montant: 999, ville: "berlin"}}]

recompute = fn j ->
  j
  |> Enum.filter(fn ev -> match?({:achat, %{ville: "paris"}}, ev) end)
  |> Enum.map(fn {:achat, %{montant: m}} -> m end)
  |> Enum.sum()
end

if recompute.(journal_bruite) != 95 do
  raise "des événements hors-Paris ou non-achats ont pollué le total (#{recompute.(journal_bruite)} ≠ 95) — votre filtre laisse passer trop de choses"
end

# propriété de linéarité : dupliquer chaque achat parisien double le total
double = journal ++ Enum.filter(journal, fn ev -> match?({:achat, %{ville: "paris"}}, ev) end)
if recompute.(double) != 190 do
  raise "en doublant les achats parisiens, le total devrait doubler (attendu 190, obtenu #{recompute.(double)})"
end

IO.puts("✅ Rapport correct : total_paris = #{total_paris}, robuste au bruit et linéaire")
```

> En une phrase : pourquoi devez-vous **filtrer sur la forme** `{:achat,
> %{ville: "paris"}}` *avant* de faire `Enum.map(fn {:achat, %{montant: m}}
> -> m end)`, et que se passerait-il si vous inversiez ces deux étapes sur un
> journal contenant une `:connexion` ?
>
> Votre réponse : …
