AP – Elixir – Mini-projet · Typeur

Raisonner sans exécuter

Des règles de types à l’inférence Hindley–Milner

Projet AP9 · inférer les types de notre langage

Projet AP9 · diaporama de référence
AP – Elixir – Mini-projet · Typeur

Que peut-on savoir sur ce programme ?

let total = 12.0 * 3.0
case total < 30.0 {
  True -> total
  False -> total * 0.9
}

Le typeur peut annoncer Float sans calculer le tarif.

Projet AP9 · diaporama de référence
AP – Elixir – Mini-projet · Typeur

Un type décrit des usages possibles

12.0 et 3.0 sont des valeurs différentes du type Float.
True a le type Bool.

fn(Float) → Bool décrit une fonction qui reçoit un nombre et rend un booléen.

Notre typeur ne choisit aucune valeur pour exécuter la fonction.

Projet AP9 · diaporama de référence
AP – Elixir – Mini-projet · Typeur

Quatre carnets, trois paliers

  1. Écrire les règles pour les formes déjà connues · bronze
  2. Résoudre des égalités entre types inconnus · argent
  3. Déduire les types des fonctions et des appels · argent
  4. Réutiliser une liaison let à plusieurs types · or

Le squelette mini-typeur fournit le lexer, le parser et les tests de chaque palier.

Projet AP9 · diaporama de référence
AP – Elixir – Mini-projet · Typeur

Comment mener le projet

  • Suivez les quatre carnets dans l’ordre : une question, un exemple, puis l’algorithme à écrire sur des arbres manuels.
  • Reportez votre code dans le squelette (lib/) et lancez mix test, puis --include argent, puis --include argent --include or.
  • Terminez le bronze avant d’attaquer l’argent ; committez chaque palier qui passe.
  • Avant chaque carnet, une page rappelle les formes Elixir utiles.

Pour relire : poly, chap. 2 (motifs, case) et chap. 3 (tuples, listes, maps).

Projet AP9 · diaporama de référence
AP – Elixir – Mini-projet · Typeur

01 · Les opérations imposent leurs règles

Expression Ce qu’elle exige Type du résultat
a + b, a - b, a * b, a / b Deux Float Float
a < b, a == b Deux Float Bool
case test { … } Un test Bool Type commun des branches

Nous avons choisi une égalité numérique, sans surcharge.

Projet AP9 · diaporama de référence
AP – Elixir – Mini-projet · Typeur

Bientôt, un contexte pour les noms

L’environnement de l’interpréteur contient les valeurs tandis que celui du typeur contient Float et Bool.

À l’étape 03, nous chercherons le type d’un nom dans un environnement.

Projet AP9 · diaporama de référence
AP – Elixir – Mini-projet · Typeur

Le typeur examine les deux branches

case True {
  True -> 1.0
  False -> False
}

Ce programme s’exécute en AP2. Notre règle de typage le refuse.
Pourquoi ne pas ignorer la branche fausse ?

Projet AP9 · diaporama de référence
AP – Elixir – Mini-projet · Typeur

Le parcours suit la forme de l’AST

Pour une opération, déduire les types de ses deux enfants puis vérifier la règle.

Pour une négation, vérifier que l’enfant est un nombre.

Pour un case, vérifier le test et comparer les deux branches.

Projet AP9 · diaporama de référence
AP – Elixir – Mini-projet · Typeur

Elixir pour ce carnet · 01

def infer({:number, _value}), do: {:ok, :float}
def infer({:binary, op, left, right}) when op in [:plus, :minus, :multiply, :divide] do
  with {:ok, :float} <- infer(left),
       {:ok, :float} <- infer(right) do
    {:ok, :float}
  else
    {:ok, other} -> {:error, {:expected_float, other}}
    error -> error
  end
end

Une clause par nœud ; with exige :float des deux côtés et explique un refus dans else.

Projet AP9 · diaporama de référence
AP – Elixir – Mini-projet · Typeur

Carnet 01 · Écrire les règles

Ouvrez le carnet 1, Que peut-on savoir sans exécuter ? · palier bronze.

Écrivez le typeur des nombres, booléens, opérations et choix.
Inventez un programme accepté et un refus à expliquer.

Commencez par les AST : aucune lecture de texte supplémentaire n’est nécessaire.

Projet AP9 · diaporama de référence
AP – Elixir – Mini-projet · Typeur

Reprise · Un type valide garantit-il le résultat ?

1.0 / 0.0

La division reçoit bien deux nombres.
Son type est Float, mais son exécution échoue.

Une règle de types garantit des propriétés précises, pas tout comportement.

Projet AP9 · diaporama de référence
AP – Elixir – Mini-projet · Typeur

02 · Que faire d’un type encore inconnu ?

fn(x) { x + 1.0 }

À l’entrée, notons le type de x par t0.
L’opération + impose ensuite t0 = Float.

Une inconnue n’est pas le type « n’importe quoi ».

Projet AP9 · diaporama de référence
AP – Elixir – Mini-projet · Typeur

Les nœuds produisent des contraintes

Le paramètre x reçoit t0 et x plus 1.0 impose t0 égal à Float ; la fonction devient Float vers Float.

Une contrainte est une égalité que notre solveur devra rendre vraie.

Projet AP9 · diaporama de référence
AP – Elixir – Mini-projet · Typeur

Une substitution relie les inconnues

La substitution t0 vers t1 puis t1 vers Float doit être suivie jusque dans les paramètres et résultats de fonction.

resolve suit les liaisons et descend dans la structure du type.

Projet AP9 · diaporama de référence
AP – Elixir – Mini-projet · Typeur

Unifier, c’est rendre deux types compatibles

Après résolution des deux types Action
Ils sont identiques Ne rien ajouter
L’un est une inconnue libre La lier si elle ne se contient pas
Ce sont deux fonctions Unifier les paramètres puis les résultats
Les formes sont incompatibles Refuser

Deux fonctions doivent aussi avoir le même nombre de paramètres.

Projet AP9 · diaporama de référence
AP – Elixir – Mini-projet · Typeur

Déroulons une unification

L’unification de fn(t0) vers t0 avec fn(Float) vers t1 lie t0 à Float puis applique cette substitution avant de lier t1.

Chaque nouvelle égalité est résolue avec les informations déjà obtenues.

Projet AP9 · diaporama de référence
AP – Elixir – Mini-projet · Typeur

Pourquoi refuser une inconnue qui se contient ?

L’auto-application x(x) impose t0 égal à fn(t0) vers t1, ce qui demande un type infini.

L’occurs-check cherche l’inconnue dans le type proposé avant de la lier.

Projet AP9 · diaporama de référence
AP – Elixir – Mini-projet · Typeur

Elixir pour ce carnet · 02

sub = %{0 => {:var, 1}, 1 => :float}     # la substitution : identifiant → type
Map.get(sub, id)                         # nil si l'inconnue est encore libre
Map.put(sub, id, type)                   # lier, en rendant une nouvelle map
  • resolve(type, sub) suit les liaisons jusqu'à un type sans entrée dans la map, puis descend dans {:fun, args, result}.
  • MapSet représente un ensemble d'identifiants : MapSet.member?/2, MapSet.put/2.
Projet AP9 · diaporama de référence
AP – Elixir – Mini-projet · Typeur

Carnet 02 · Résoudre les contraintes

Ouvrez le carnet 2, Résoudre des contraintes · palier argent.

Écrivez resolve, l’occurs-check et unify.
Tracez les substitutions après chaque égalité.

Testez une chaîne de remplacements, un conflit et une auto-application.

Projet AP9 · diaporama de référence
AP – Elixir – Mini-projet · Typeur

Reprise · Qu’est-ce qu’une solution ?

Avec t0 = t1 puis t1 = Float, que donne resolve(t0) ?

Avec fn(t0) → t0 = fn(Float) → Bool, où apparaît le conflit ?

Expliquez pourquoi l’ordre des contraintes ne doit pas changer leur sens.

Projet AP9 · diaporama de référence
AP – Elixir – Mini-projet · Typeur

03 · Une fonction relie plusieurs types

fn(x) { x }
fn(x) { x + 1.0 }
fn(x) { x < 10.0 }

Nous voulons déduire respectivement :
fn(t0) → t0, fn(Float) → Float, fn(Float) → Bool.

Projet AP9 · diaporama de référence
AP – Elixir – Mini-projet · Typeur

À chaque paramètre, une inconnue fraîche

Pour fn(x) { body } :

  1. Créer une inconnue pour x.
  2. Ajouter x à l’environnement du corps.
  3. Déduire le type de body.
  4. Former le type de fonction, puis le résoudre.

Chaque lecture de ce paramètre garde la même inconnue.

Projet AP9 · diaporama de référence
AP – Elixir – Mini-projet · Typeur

Un appel impose aussi une forme de type

fn(f) { f(1.0) }

Appelons t0 le type de f, et t1 le type du résultat.
L’appel impose t0 = fn(Float) → t1.

La fonction entière a le type fn(fn(Float) → t1) → t1.

Projet AP9 · diaporama de référence
AP – Elixir – Mini-projet · Typeur

Le solveur retrouve le résultat d’un appel

(fn(x) { x + 1.0 })(4.0)

Le type de la fonction est fn(Float) → Float.
L’appel demande fn(Float) → t0.

L’unification impose donc t0 = Float.

Projet AP9 · diaporama de référence
AP – Elixir – Mini-projet · Typeur

Le parcours transporte deux formes de contexte

L’environnement associe les noms du programme à des types.

La substitution associe les inconnues à ce que nous avons appris.

Le compteur crée des inconnues distinctes. Il continue d’avancer, même quand nous quittons un bloc.

Projet AP9 · diaporama de référence
AP – Elixir – Mini-projet · Typeur

Elixir pour ce carnet · 03

state = %{fresh: 0, sub: %{}}            # le compteur et la substitution voyagent ensemble
{{:var, state.fresh}, %{state | fresh: state.fresh + 1}}
Enum.map_reduce(arguments, state, fn argument, state -> infer(argument, env, state) end)
  • L'état est une map ; une inconnue fraîche rend le type et le nouvel état.
  • Enum.map_reduce/3 transforme chaque argument tout en faisant avancer l'état.
Projet AP9 · diaporama de référence
AP – Elixir – Mini-projet · Typeur

Carnet 03 · Fonctions et appels

Ouvrez le carnet 3, Inférer des fonctions · palier argent.

Reprenez votre typeur et votre solveur. Ajoutez les inconnues fraîches, les fonctions et les appels.

Déduisez le type d’une identité, d’une composition simple et d’un appel invalide.

Projet AP9 · diaporama de référence
AP – Elixir – Mini-projet · Typeur

Reprise · L’identité est-elle vraiment réutilisable ?

let id = fn(x) { x }
let number = id(1.0)
id(True)

Si les deux usages partagent t0, le premier fixe t0 = Float.
Le deuxième provoque alors un conflit.

Projet AP9 · diaporama de référence
AP – Elixir – Mini-projet · Typeur

04 · Garder une recette de type

Pour let id = fn(x) { x }, nous conservons le schéma :

∀a. fn(a) → a

À chaque lecture de id, nous créons une nouvelle instance.
À l’intérieur de cette instance, l’argument et le résultat restent liés.

Projet AP9 · diaporama de référence
AP – Elixir – Mini-projet · Typeur

Les deux usages de id ont leurs inconnues

Le schéma de l’identité est instancié avec t1 pour le nombre et t2 pour le booléen, tout en liant argument et résultat dans chaque appel.

La généralisation crée le schéma ; l’instanciation crée des inconnues fraîches.

Projet AP9 · diaporama de référence
AP – Elixir – Mini-projet · Typeur

Quelles inconnues pouvons-nous généraliser ?

L’identité généralise t0 absent de l’environnement ; un alias de paramètre ne généralise aucune inconnue déjà présente dans le contexte.

Nous quantifions libres(type) − libres(environnement), après résolution.

Projet AP9 · diaporama de référence
AP – Elixir – Mini-projet · Typeur

Un paramètre reste monomorphe

Les deux appels d’un paramètre f, même via let g = f, partagent les mêmes inconnues et ne peuvent imposer Float puis Bool.

let g = f ne rend pas polymorphes les inconnues appartenant déjà au contexte.

Projet AP9 · diaporama de référence
AP – Elixir – Mini-projet · Typeur

Deux opérations aux moments précis

Au moment d’un let Au moment de lire un nom
Inférer le type de sa valeur Retrouver son schéma
Résoudre type et environnement Créer une inconnue par variable quantifiée
Généraliser les inconnues indépendantes Remplacer leurs occurrences de façon cohérente
Lier le schéma pour la suite Rendre cette instance

La portée des noms reste celle d’AP2.

Projet AP9 · diaporama de référence
AP – Elixir – Mini-projet · Typeur

Comment les briques s’appellent-elles ?

Le visiteur produit les contraintes, unify enrichit la substitution, let généralise et les lectures de noms instancient.

Le visiteur suit l’AST ; le solveur relie les types et transmet son état.

Projet AP9 · diaporama de référence
AP – Elixir – Mini-projet · Typeur

Elixir pour ce carnet · 04

free = MapSet.difference(free_vars(type), free_vars_env(env))   # quoi généraliser
scheme = {:scheme, MapSet.to_list(free), type}
renaming = Enum.reduce(ids, %{}, fn id, map -> Map.put(map, id, fresh) end)
  • MapSet.difference/2 : les inconnues du type, moins celles de l'environnement.
  • Instancier : construire d'abord la map ancien id → nouvelle inconnue, puis remplacer partout.
Projet AP9 · diaporama de référence
AP – Elixir – Mini-projet · Typeur

Carnet 04 · Le polymorphisme de let

Ouvrez le carnet 4, Réutiliser une fonction à plusieurs types · palier or.

Écrivez variables libres, généralisation et instanciation.
Faites accepter l’identité aux deux types, puis refuser le paramètre et son alias.

Expliquez précisément quelles inconnues peuvent être renommées.

Projet AP9 · diaporama de référence
AP – Elixir – Mini-projet · Typeur

Reprise · Déduire le type le plus général possible

let keep = fn(x) { x }
keep

Aucune opération n’impose Float ou Bool.
Le type garde la relation entre l’argument et le résultat.

Quel ajout dans le corps forcerait une spécialisation ?

Projet AP9 · diaporama de référence
AP – Elixir – Mini-projet · Typeur

Ce que notre système laisse encore ouvert

Une division par zéro reste possible.
Un type ne dit pas combien de temps prendra le calcul.

Nous avons choisi un langage pur, sans sous-typage ni opérateurs surchargés.
Les liaisons récursives demanderaient une règle supplémentaire.

Projet AP9 · diaporama de référence
AP – Elixir – Mini-projet · Typeur

Pour aller plus loin

Ajoutez des paires et leurs projections.
Améliorez le diagnostic en gardant l’origine d’une contrainte.
Ou vérifiez une annotation de type donnée par l’auteur du programme.

Pour chaque extension, commencez par un exemple accepté et un refus.

Projet AP9 · diaporama de référence
AP – Elixir – Mini-projet · Typeur

Comparez vos trois moteurs

Pour un même programme :

  • l’interpréteur d’AP2 rend une valeur ;
  • la machine d’AP3 rend un résultat, ou une erreur avec son PC ;
  • votre typeur rend un type, ou un refus avant toute exécution.

Cherchez un programme accepté par les trois, un programme refusé seulement par le typeur, un programme qui n’échoue qu’à l’exécution.

Projet AP9 · diaporama de référence
AP – Elixir – Mini-projet · Typeur

Garder et expliquer votre progression

Je préciserai en cours les carnets ou les fichiers à terminer et le point d’arrêt.

Gardez un programme accepté, un refus expliqué et une limite du système dans COMPTE-RENDU.md.
Les paliers et les tests servent à choisir la prochaine étape ; ils ne sont pas un barème.

Les prolongements non demandés restent facultatifs ; les modalités seront annoncées en cours.

Quand un bout de syntaxe manque : le poly, avec son aide-mémoire.

Projet AP9 · diaporama de référence

Diaporama de référence du mini-projet, à lire en autonomie avec les quatre carnets guidés et le squelette mini-typeur. Les paliers bronze, argent et or correspondent au carnet 1, aux carnets 2 et 3, puis au carnet 4 ; ils décrivent l’ordre des idées, pas un barème.

Lancement. L’interpréteur produit 32.4 (le tarif 36.0, remisé de dix pour cent), le typeur produit Float sans faire ce calcul. Nous conservons les mêmes AST qu’en AP2, mais les notebooks partent d’arbres manuels : un parser terminé n’est pas requis. L’objectif est de comprendre comment des règles locales donnent une information globale.

Lancement. Donner la notation de fonction avant les algorithmes d’unification. Le nom des types ne fait pas partie des valeurs du langage cible. Distinguer syntaxe cible fn(x){...} et notation de types fn(Float) → Bool. Un type est une donnée du programme Elixir que nous écrivons.

Les inconnues permettent de travailler sans annotations : c’est l’inférence de types, qui déduit les types au lieu de les lire. Présenter Hindley–Milner comme le système que nous construirons dans notre petit langage pur : pas de sous-typage ni de surcharge des opérateurs, c-à-d. qu’un opérateur n’a qu’un seul type. Chaque carnet reprend les modules personnels du précédent ; le code final se reporte dans le squelette.

Annoncer en cours le palier choisi et le point d’arrêt. Les carnets et le squelette sont deux supports de la même progression, pas deux devoirs. Les commandes sont cumulatives : --include or seul laisserait les tests argent exclus. Les formes Elixir rappelées sont case, with, les maps et MapSet ; quand un bout de syntaxe manque en route, le poly et son aide-mémoire les reprennent, sans être un prérequis.

Carnet 1, première partie. Donner un seul exemple de règle détaillée puis faire dériver les autres. True == False est refusé dans notre langage. Une égalité surchargée accepterait plusieurs types pour le même opérateur ; ce choix évite ses contraintes supplémentaires.

Carnet 01, aperçu de la suite seulement. Le notebook 01 travaille les nombres, booléens, opérations et case ; noms et let arrivent à l’étape 03. Faire distinguer la nature des deux informations, sans demander d’implémenter leur environnement maintenant. Les types sont des données Elixir : :float et :bool.

Carnet 01. Les deux branches doivent donner le même type quelle que soit la valeur du test. Nous ne réduisons pas le programme pendant l’inférence pour supprimer les branches mortes. L’AST est syntaxiquement correct : le souci ne vient ni du lexer ni du parser.

Le parcours renvoie {:ok, type} ou une erreur explicite. L’élève écrit les clauses analogues, pas un trou dans une grande fonction visit déjà fournie. Les erreurs gardent assez de contexte pour expliquer la règle qui échoue.

Même structure que l'évaluateur d'AP2, mais le résultat est un type, pas une valeur. Le with … else … permet d'expliquer la règle qui échoue. Les élèves écrivent les clauses analogues : négation, comparaisons, case.

Les exemples du langage cible sont associés à des AST compacts écrits à la main. Écrire toutes les clauses peut être dense : le raisonnement et les contre-exemples priment sur la couverture mécanique de chaque opérateur.

reprise de 5 min. Faire nommer ce que les règles garantissent : catégories de valeurs cohérentes pour les opérateurs, les branches puis les appels. Ne pas promettre absence de toute erreur d’exécution ou terminaison. Analyser la valeur zéro demanderait une autre propriété.

Carnet 2. fn motive l’inconnue ; son inférence viendra au carnet 03. Toutes les occurrences de t0 décrivent une même inconnue à résoudre. Un paramètre ne change pas arbitrairement de type à chaque lecture.

Carnet 02. Visiter un AST et unifier ses contraintes sont deux tâches séparables. Nous écrivons d’abord le solveur sur des types manuels. La référence résout au fil du parcours, sans conserver nécessairement une liste de toutes les contraintes.

Carnet 02. Représentations {:var, id}, :float, :bool, {:fun, args, result}. Exemple de substitution : %{0 => {:var, 1}, 1 => :float}. Ne pas confondre remplacement d’une variable de type et valeur d’un paramètre à l’exécution. L’occurs-check empêche les chaînes cycliques.

Carnet 02. Tester l’égalité avant l’occurs-check, sinon unify(t0, t0) serait refusé à tort. Après chaque unification, transmettre la nouvelle substitution au couple suivant. Float et Bool restent distincts. Cette table donne les décisions de l’algorithme ; le code reste à écrire.

Carnet 02. Masquer la dernière ligne et faire prédire la substitution. Après t0 = Float, la contrainte t0 = t1 devient Float = t1. Unifier seulement les paramètres, oublier le résultat ou résoudre chaque paire avec une substitution séparée perdrait une information essentielle.

L’occurs-check descend dans tous les paramètres et le résultat, après résolution. L’auto-application sera générée par le visiteur de l’étape 03 ; ici, donner la paire de types comme contre-exemple du solveur. L’égalité t0 = t0 réussit avant cette vérification.

La substitution est une map persistante : chaque unify rend une nouvelle map, aucune mutation. L'occurs-check parcourt le type résolu ; un MapSet évite les doublons si l'on collecte les inconnues.

Contrat unify(a, b, sub) → {:ok, sub} ou erreur. Ce sont de vrais algorithmes courts à construire, pas une comparaison isolée à compléter. Le solveur se teste indépendamment du typeur de l’étape 01.

reprise de 5 min. Premier résultat Float ; second conflit Float/Bool après résolution. Les identifiants et la forme intermédiaire de la map peuvent varier, mais les types résolus doivent exprimer la même information. Ne pas exiger une normalisation canonique de la substitution.

Carnet 3. t0 est une variable de type, pas un type concret appelé t0. Le premier type est aussi général que le permet l’identité. Les inconnues d’un exemple sont indépendantes de celles des autres.

Carnet 03. L’état transmet le compteur fresh et la substitution. Plusieurs paramètres reçoivent des inconnues distinctes ; l’arité zéro existe aussi dans la référence. Le corps peut imposer Float. Les paramètres resteront monomorphes quand let deviendra polymorphe.

Carnet 03. Ne pas attendre de connaître le résultat pour former le type d’appel : créer une inconnue fraîche puis unifier. Le paramètre f pourrait rendre Bool ou Float ; rien n’impose davantage ici. Les appels ne sont pas curryfiés : le nombre d’arguments doit correspondre.

Carnet 03. Le parser de référence accepte cette fonction parenthésée puis appelée. Le TP utilise son AST pour éviter une dépendance au parser. Remplacer 4.0 par True révèle Float = Bool ; deux arguments révèlent une erreur d’arité.

Les noms suivent la portée lexicale ; la substitution et le compteur traversent les sous-arbres. Les environnements du parent et du corps restent distincts. La substitution doit aussi être appliquée au résultat final.

Trois informations circulent : env suit la portée (comme en AP2), state traverse tout le parcours. map_reduce est un reduce qui garde aussi la liste des résultats ; une récursion explicite convient aussi.

MiniTypes.infer(ast) rend {:ok, type}. Les élèves relient eux-mêmes les mécanismes, sans recevoir tout visit puis un trou de contrainte. let reste monomorphe à cette étape pour faire apparaître la question suivante.

reprise de 5 min. Ce programme est valide avec le polymorphisme de l’étape 04, mais refusé par la version monomorphe de 03. Faire produire Float = Bool. Le paramètre x conserve un type unique pendant l’inférence du corps.

Carnet 4. Lire ∀a comme « pour tout type a ». Un schéma n’est pas un type any. Seules les inconnues quantifiées sont remplacées ; les autres restent partagées avec l’environnement.

Carnet 04. Première instance fn(t1) → t1, puis t1 = Float. Deuxième instance fn(t2) → t2, puis t2 = Bool. Une variable répétée du schéma reçoit la même inconnue fraîche dans une instance. Garder le schéma dans l’environnement, sans le remplacer par sa première instance.

Carnet 04. Les variables libres d’un schéma excluent celles déjà quantifiées. À droite, supposons que f ait reçu le type fn(t0) → t1 par un usage : ses variables figurent dans le type de g et dans l’environnement. Résoudre aussi l’environnement avant la différence, sinon une liaison indirecte peut être généralisée à tort.

Carnet 04. Lire le programme du schéma. let peut généraliser ; les paramètres ne le font pas. Représenter un paramètre par un schéma avec une liste vide de variables quantifiées. L’alias g conserve ce partage, contrairement à let id = fn(x) { x }.

Carnet 04. Contrats generalize(type, env, sub) → {ids, type} et instantiate(scheme, state) → {type, state}, sans donner leur implémentation. La substitution appliquée à un schéma protège ses variables quantifiées. Notre let non récursif n’est pas visible dans sa propre valeur.

Carte de la référence : visit, constrain, unify, resolve, generalize, instantiate. Les élèves peuvent employer d’autres petits auxiliaires ; identifier la responsabilité plutôt que chercher tous ces noms mécaniquement. MiniTypes.infer rend un type final résolu.

Tout est résolu avant la différence d'ensembles. L'instanciation remplace de façon cohérente : deux occurrences du même identifiant reçoivent la même inconnue fraîche.

Le principal risque est une généralisation excessive. Les contre-exemples sont centraux ; dessiner les ensembles libres avant de montrer du code à un élève bloqué. Il s’agit d’un vrai polymorphisme let, pas d’un redémarrage du typeur à chaque appel.

reprise de 5 min. Résultat fn(t) → t. Ajouter x + 1.0 impose Float. Ce type est le plus général dans notre système Hindley–Milner, pas dans tous les langages possibles. À la lecture de keep, le schéma est instancié.

Types Float, Bool et fonctions, sans égalité polymorphe ni collection. Aucune restriction de valeur à ajouter dans ce langage pur. Distinguer les garanties de notre système, une analyse plus précise et les propriétés d’exécution.

Pour aller plus loin, hors paliers. Une paire ajoute un constructeur de type à resolve, unify et aux variables libres : bon exemple d’ajouts analogues. La récursion monomorphe dans son propre corps est un défi ultérieur, distinct du polymorphisme de let.

Trois réponses attendues : un calcul simple ; case True { True -> 1.0 False -> False } ; 1.0 / 0.0. Cette comparaison fait un bon paragraphe de README du projet.

Une explication du partage des inconnues compte davantage qu’une implémentation recopiée. Le compte rendu se rédige au fil du travail : commandes, trace de contraintes, occurs-check, différence entre paramètre et let polymorphe, comparaison des trois moteurs. Le projet noté se poursuit à la maison ; périmètre, barème et échéance seront précisés avant AP9.