# AP2 · 4 — Emporter le lieu de définition

## Notre question

Notre fonction a perdu la liaison de `offset`. Quelle information faudrait-il
ajouter à sa valeur pour faire marcher ce programme ?

```text
let offset = 2.0
let add_offset = fn(x) { offset + x }
let offset = 100.0
add_offset(3.0)
```

Deux règles sont possibles, et des langages ont existé avec chacune. La
portée *dynamique* prendrait les noms chez l'appelant : `offset` vaudrait
`100.0` au moment de l'appel, et le résultat serait `103.0`. Nous choisissons
la portée **lexicale** : `offset` dans le corps désigne la liaison visible à
la définition, et le résultat sera donc `5.0`. Prédisez aussi
`add_offset(offset)` avec les mêmes définitions. Les deux `offset` servent-ils
au même moment et dans le même environnement ?

> **Repères.** [Slides 37 à 45](https://ap-2026.noce.fr/slides/AP2.html#37) : portée dynamique ou lexicale, fermeture, fonction qui fabrique une fonction, capturer une variable ou une valeur. Elixir : `++` et les chaînes `"""`, expliqués au point d'emploi ; sinon, seule la valeur produite change. Poly, chap. 5, § 5.7 « Fonctions et fermetures ».

> 💡 Cette étape permet d'approfondir notre interpréteur.
> Selon notre rythme, nous pourrons la travailler ensemble ou la
> garder pour la suite. Le travail à rendre sera annoncé en cours ;
> les prolongements non demandés restent des bonus facultatifs.
> **Sauvegardez-le dès maintenant sous `AP2/ap2-04-fermetures.livemd`.**

## Reprendre votre travail

Ouvrez votre notebook `ap2-03-contexte-fonctions.livemd` à côté de celui-ci. Une nouvelle
session Livebook ne connaît aucun de vos modules précédents. Copiez vos
cellules **dans les trois cellules indiquées**, puis évaluez-les de haut
en bas. Conservez vos versions dans ce notebook : il ne chargera aucun
fichier ni aucun code sur Internet.

Après la mise en commun, repartez de votre version discutée en classe,
même si elle ne couvre pas encore toutes les extensions. Le cœur de cette
étape utilise des AST écrits à la main et peut avancer sans parser terminé.
Si votre évaluateur bloque encore, construisez d'abord une version limitée
aux cas utilisés ci-dessous ; signalez cette limite dans votre explication.

```elixir
# Copiez ici votre module ArithmeticOps (les opérations sur les valeurs).
```

```elixir
# Copiez ici Bindings puis Interpreter ; modifiez Interpreter dans cette cellule.
```

```elixir
# Pour le parcours depuis le texte : copiez vos modules de lecture des littéraux,
# ArithmeticInput et ArithmeticParser. Cette cellule peut attendre le prolongement.
```

Les vérifications de ce notebook utilisent `assert`, fourni par Elixir via
ExUnit : une condition fausse arrête la cellule et affiche la valeur obtenue
à côté de celle attendue. Évaluez la cellule d'import une fois, puis
réévaluez votre code avant ses tests après chaque modification.

```elixir
import ExUnit.Assertions
```

Cette première vérification confirme que votre évaluateur recopié répond encore sur un arbre de l'étape 2 :

```elixir
assert Interpreter.eval({:binary, :plus, {:number, 2.0}, {:number, 3.0}}) == {:ok, 5.0}
```

Dans la cellule `Interpreter`, copiez aussi votre module `Bindings` de l'étape 3.
Gardez les cas arithmétiques, les noms, `let` et votre lecture des arguments.
Remplacez les **deux algorithmes** qui créent et appellent une fonction.

## Concevoir une fermeture

Une *fermeture* est la valeur d'une fonction qui emporte son contexte de
définition. Nous allons utiliser la valeur `{:closure, parameters, body, captured}` :
`captured` est l'environnement visible quand nous évaluons le nœud `fn`.
Il n'y a aucune mutation de cet environnement quand un nouveau `let` masque un nom ;
la liste capturée reste celle du moment de la définition.

Écrivez d'abord sur papier les environnements de définition et d'appel du programme.
Puis écrivez vos clauses dans `Interpreter`. Il faut décider :

- quel environnement conserver à la définition ;
- où évaluer la fonction et chacun de ses arguments ;
- comment construire l'environnement du corps avec les paramètres et les liaisons capturées.

Les paramètres doivent masquer les noms capturés de même nom. Gardez les erreurs
de l'étape 3 (arité, valeur non appelable, erreur d'argument). Une fermeture ne
rend pas automatiquement `let` récursif : son propre nom n'est pas encore lié
quand sa valeur est construite.

> 💡 `liste1 ++ liste2` construit la liste formée des éléments de la première
> puis de ceux de la seconde : `[{"x", 3.0}] ++ [{"offset", 2.0}]` donne
> `[{"x", 3.0}, {"offset", 2.0}]`. Comme `Bindings.fetch/2` prend la première
> liaison qui correspond, les couples placés à gauche masquent ceux de droite.

Prédisez les deux résultats ci-dessous avant d'évaluer : dans le second, quel
`offset` l'argument lit-il, et quel `offset` le corps lit-il ?

```elixir
add_offset = {:fn, ["x"], {:binary, :plus, {:name, "offset"}, {:name, "x"}}}
lexical = {:let, "offset", {:number, 2.0},
  {:let, "add_offset", add_offset,
    {:let, "offset", {:number, 100.0},
      {:call, {:name, "add_offset"}, [{:number, 3.0}]}}}}
assert Interpreter.eval(lexical) == {:ok, 5.0}
caller_argument = {:let, "offset", {:number, 2.0},
  {:let, "add_offset", add_offset,
    {:let, "offset", {:number, 100.0},
      {:call, {:name, "add_offset"}, [{:name, "offset"}]}}}}
assert Interpreter.eval(caller_argument) == {:ok, 102.0}
```

## Fabriquer une fonction qui survit à son appel

Quel résultat prévoyez-vous ? Où retrouver `x` quand `add_two` est appelé, puisque
l'appel de `make_adder` est terminé ? Dessinez deux fermetures différentes obtenues
avec `make_adder(2.0)` et `make_adder(10.0)` : même corps, contextes distincts.

```text
let make_adder = fn(x) { fn(y) { x + y } }
let add_two = make_adder(2.0)
add_two(5.0)
```

Les deux dernières lignes de la cellule appellent la fermeture obtenue sous le
nom `f`, en la plaçant directement dans l'environnement initial de `eval/2`.

```elixir
make_adder = {:fn, ["x"], {:fn, ["y"], {:binary, :plus, {:name, "x"}, {:name, "y"}}}}
factory = {:let, "make_adder", make_adder,
  {:let, "add_two", {:call, {:name, "make_adder"}, [{:number, 2.0}]},
    {:call, {:name, "add_two"}, [{:number, 5.0}]}}}
assert Interpreter.eval(factory) == {:ok, 7.0}
{:ok, add_two} = Interpreter.eval({:call, make_adder, [{:number, 2.0}]})
{:ok, add_ten} = Interpreter.eval({:call, make_adder, [{:number, 10.0}]})
assert Interpreter.eval({:call, {:name, "f"}, [{:number, 5.0}]}, [{"f", add_two}]) == {:ok, 7.0}
assert Interpreter.eval({:call, {:name, "f"}, [{:number, 5.0}]}, [{"f", add_ten}]) == {:ok, 15.0}
```

Ces dernières vérifications reprennent les erreurs de l'étape 3 et ajoutent un
cas de masquage : un paramètre `x` doit gagner sur un `x` capturé.

```elixir
# Un paramètre masque une liaison capturée ; le nombre d'arguments reste contrôlé.
identity = {:fn, ["x"], {:name, "x"}}
assert Interpreter.eval({:call, identity, [{:number, 3.0}]}, [{"x", 100.0}]) == {:ok, 3.0}
assert Interpreter.eval({:call, identity, []}) == {:error, {:arity, 1, 0}}
assert Interpreter.eval({:call, {:number, 1.0}, []}) == {:error, :not_a_function}
assert Interpreter.eval({:call, identity, [{:name, "missing"}]}) ==
  {:error, {:undefined_name, "missing"}}
```

### Si vous bloquez

Premier indice : la fermeture emporte une liste de liaisons, pas une liste de
noms seuls. Deuxième indice : les expressions des arguments appartiennent au
programme de l'appelant. Troisième indice : les nouvelles liaisons des paramètres
se placent avant celles de l'environnement capturé.

## Revenir au texte

Le parser n'a pas besoin de changer : `fn` et l'appel ont les mêmes AST qu'avant.
C'est la **valeur produite par l'évaluateur** qui devient plus riche. Si vous
avez poursuivi l'extension syntaxique de l'étape 3, exécutez la cellule suivante.

> 💡 Une chaîne entre triples guillemets `"""` s'étend sur plusieurs lignes,
> fins de ligne comprises ; c'est commode pour écrire un programme entier.
> Notre lexer ignore ces fins de ligne comme des blancs.

```elixir
source = """
let make_adder = fn(x) { fn(y) { x + y } }
let add_two = make_adder(2.0)
add_two(5.0)
"""
{:ok, tokens} = ArithmeticInput.lex(source)
{:ok, tree} = ArithmeticParser.parse(tokens)
assert Interpreter.eval(tree) == {:ok, 7.0}
```

## Avant de reprendre les slides

Expliquez en deux phrases pourquoi `add_offset(3.0)` donne `5.0` mais
`add_offset(offset)` donne `102.0` dans le premier exemple. Ajoutez un contre-exemple
qui échouerait si le corps prenait l'environnement de l'appelant, puis un autre
si les arguments étaient évalués dans l'environnement capturé. Retenez qu'il y a
deux environnements à chaque appel, celui de l'appelant pour les arguments et
celui de la définition pour le corps, et que la fermeture est la donnée qui
conserve le second.

Écrivez vos réponses dans une cellule Markdown de ce notebook, avec votre
contre-exemple et ce qu'il vous a appris. Sauvegardez ce notebook dans `AP2/`.
Pour le rendu unique du cycle AP1–AP3, je préciserai en cours les étapes
à terminer et le résultat minimum attendu. Les numéros indiquent l'ordre
de progression, pas une liste de devoirs. Les prolongements non demandés
restent facultatifs et peuvent apporter un bonus.
L'échéance commune est le 6 décembre 2026 à 23 h 59 (Paris) ; il n'y a pas
de devoir intermédiaire. AP3 s'ouvrira par un quiz de dix minutes sur
cette séance : relisez le chapitre 5 du poly, « Construire un langage ».

Si un bout de syntaxe Elixir vous manque en route, les chapitres 1 à 4 du
[poly](https://ap-2026.noce.fr/poly/poly-elixir.pdf) se consultent au besoin ; ils ne sont pas un prérequis de cette
étape.

Pour toute question, vous pouvez me contacter par email :
[aurelien.noce@minesparis.psl.eu](mailto:aurelien.noce@minesparis.psl.eu).
