Programmation Fonctionnelle Avancée 7 : Inférence de types, polymorphie et traits impératifs Programmation Fonctionnelle Avancée 7 : Inférence de types, polymorphie et traits impératifs Le typage en OCaml Programmation Fonctionnelle Avancée 7 : Inférence de types, polymorphie et traits impératifs Ralf Treinen Université Paris Diderot UFR Informatique Institut de Recherche en Informatique Fondamentale Rappel sur le typage en OCaml Les deux traits essentiels du système de typage de OCaml sont : I I Système de types polymorphe : List .map manipule des listes de tout type. Les listes sont polymorphes, mais homogènes (dans chaque liste, tous les éléments ont le même type). Inférence de types : le système découvre tout seul le type le plus général, sans besoin de déclarer les types des identicateurs. [email protected] 15 novembre 2016 c Roberto Di Cosmo et Ralf Treinen Programmation Fonctionnelle Avancée 7 : Inférence de types, polymorphie et traits impératifs Le typage en OCaml Exemples (inf1.ml) Programmation Fonctionnelle Avancée 7 : Inférence de types, polymorphie et traits impératifs Le typage en OCaml Exemples (inf2.ml) L i s t . map ; ; let k x y = x ; ; L i s t . map ( fun x −> x + 1 ) [ 1 ; 2 ; 3 ] ; ; let s x y z = (x y) (y z ) ; ; L i s t . map ( fun s −> s ^"−" ) [ " a " ; "b" ; " c " ] ; ; let f x y z = x (y z) (y , z ) ; ; Programmation Fonctionnelle Avancée 7 : Inférence de types, polymorphie et traits impératifs Le typage en OCaml Programmation Fonctionnelle Avancée 7 : Inférence de types, polymorphie et traits impératifs Le typage en OCaml Inférence de types Système d'équations entre types I Comment est-ce que OCaml fait pour trouver le type le plus général d'un identicateur ? I Regardons d'abord le dernier exemple du transparent précédant. I Il s'agit d'un cas simple : pas de récurrence. I On introduit une variable pour le type de chaque identicateur nouveau (ici : pour les identicateurs f , x, y , z ), et une variable pour chacune des expressions du côté droite : I Rappel : → associe à droite, c.-à-d. : x →y →z I x → (y → z) Principe pour let f x1 ... xn = e : I I e1 est à lire comme I tf = tx 1 → . . . → txn → te si e = (e 1, e 2) : te = te 1 × te 2 si e = e 1 e 2 . . . en : te 1 = te 2 → . . . ten → te z }| { let f x y z = x (y z) (y , z) | {z } | {z } e2 I e3 Variables : tf , tx , ty , tz , t1 , t2 , t3 Programmation Fonctionnelle Avancée 7 : Inférence de types, polymorphie et traits impératifs Le typage en OCaml Le typage en OCaml Systèmes d'éqyautions entre types I Système d'équations entre types On a : e1 z }| { let f x y z = x (y z) (y , z) | {z } | {z } e2 I Programmation Fonctionnelle Avancée 7 : Inférence de types, polymorphie et traits impératifs e3 On écrit des équations, on utilisant les règles d'associativité : I Comment obtenir tf à partir de ces équations ? I On peut d'abord réécrire le système d'équations en une forme plus habituelle : tf = f (tx , f (ty , f (tz , t1 ))) tf = tx → (ty → (tz → t1 )) tx = f (t2 , f (t3 , t1 )) tx = t2 → (t3 → t1 ) ty = f (tz , t2 ) ty = tz → t2 t3 = p(ty , tz ) t3 = ty × tz où f (x, y ) remplace x → y , et p(x, y ) remplace x × y . Programmation Fonctionnelle Avancée 7 : Inférence de types, polymorphie et traits impératifs Le typage en OCaml Le typage en OCaml Le sens des équations entre types I I Dans les équations il y a des variables, et des constructeurs de types f (→) et p (×), et éventuellement des constantes (int, bool). Propriétés des constantes et constructeurs : I I I Deux termes avec des constructeurs/constantes diérentes à la tête ne peuvent jamais être égaux. p(x1 , x2 ) = p(y1 , y2 ) exactement si x1 = y1 et x2 = y2 . Pareil pour f . Ce sont précisément les lois des symboles de fonctions non interprétées comme on les connaît de la Logique ! Programmation Fonctionnelle Avancée 7 : Inférence de types, polymorphie et traits impératifs Le typage en OCaml Donné : I Resolution d'équations entre types I L'algorithme pour résoudre des équations entre termes dans une structure de symboles de fonctions non interprétées est précisément l'algorithme d'unication de Herbrand (voir un cours de Logique) ! I L'unication nous donne soit l'information que le système d'équations n'a pas de solution, soit une solution la plus générale (mgu) : toute solution peut être obtenu comme instance du mgu. I Nous cherchons le type le plus général de f qui est permis par la dénition de f , cela correspond exactement au mgu du système des équations. Programmation Fonctionnelle Avancée 7 : Inférence de types, polymorphie et traits impératifs Le typage en OCaml Rappel : L'algorithme d'unication (1) I Programmation Fonctionnelle Avancée 7 : Inférence de types, polymorphie et traits impératifs une signature Σ (un ensemble de symboles de fonction avec leur arité). Dans le cas des équations de types on a Rappel : L'algorithme d'unication (2) I Règles de transformation sur un système d'équations. I Decomposition : g (s1 , . . . , sn ) = g (t1 , . . . , tn ) s1 = t1 , . . . , sn = tn Σ = {p, f , int, bool, . . .} I où p , f ont arité 2, int et bool ont arité 0. Un ensemble V de variables. I T (Σ, V ) : l'ensemble des termes construits sur Σ et V I free(t) : l'ensemble des variables qui paraissent dans le terme t I Clash : g (s1 , . . . , sn ) = h(t1 , . . . , tm ) false quand g diérent de h Programmation Fonctionnelle Avancée 7 : Inférence de types, polymorphie et traits impératifs Le typage en OCaml Le typage en OCaml Rappel : L'algorithme d'unication (3) I Occur Check : Programmation Fonctionnelle Avancée 7 : Inférence de types, polymorphie et traits impératifs x =t false Rappel : L'algorithme d'unication (4) I Variable Orientation I Trivial Equation quand x ∈ free(t), et t est diérent de x . I Variable Elimination : x =t ∧φ x = t ∧ φ[x/t] t=x x =t quand t n'est pas une variable x =x true quand x 6∈ free(t), x ∈ free(φ). Programmation Fonctionnelle Avancée 7 : Inférence de types, polymorphie et traits impératifs Le typage en OCaml Le typage en OCaml Rappel : L'algorithme d'unication (5) I I I Programmation Fonctionnelle Avancée 7 : Inférence de types, polymorphie et traits impératifs Cet algorithme termine toujours : soit il donne false, soit un système d'équations en forme normale (aucune transformation ne s'applique). Exemple : résolution d'équations (1) I Système de départ : tf = f (tx , f (ty , f (tz , t1 ))) tx = f (t2 , f (t3 , t1 )) Le résultat est équivalent au système d'origine. ty = f (tz , t2 ) Quand l'algorithme se termine avec un résultat diérent de false : on a un système d'équations de la forme t3 = p(ty , tz ) x1 = t1 .. . xn = tn où aucun des xi parait dans les termes tj . I Après élimination de t3 : tf = f (tx , f (ty , f (tz , t1 ))) tx = f (t2 , f (p(ty , tz ), t1 )) ty = f (tz , t2 ) t3 = p(ty , tz ) Programmation Fonctionnelle Avancée 7 : Inférence de types, polymorphie et traits impératifs Le typage en OCaml Le typage en OCaml Exemple : résolution d'équations (2) I Programmation Fonctionnelle Avancée 7 : Inférence de types, polymorphie et traits impératifs Après élimination de t3 : Exemple : résolution d'équations (3) I Après élimination de ty : tf = f (tx , f (ty , f (tz , t1 ))) tf = f (tx , f (f (tz , t2 ), f (tz , t1 ))) tx = f (t2 , f (p(ty , tz ), t1 )) tx = f (t2 , f (p(f (tz , t2 ), tz ), t1 )) ty = f (tz , t2 ) ty = f (tz , t2 ) t3 = p(f (tz , t2 ), tz ) t3 = p(ty , tz ) I Après élimination de ty : I tf = f (tx , f (f (tz , t2 ), f (tz , t1 ))) tf = f (f (t2 , f (p(f (tz , t2 ), tz ), t1 )), f (f (tz , t2 ), f (tz , t1 ))) tx = f (t2 , f (p(f (tz , t2 ), tz ), t1 )) tx = f (t2 , f (p(f (tz , t2 ), tz ), t1 )) ty = f (tz , t2 ) ty = f (tz , t2 ) t3 = p(f (tz , t2 ), tz ) t3 = p(f (tz , t2 ), tz ) Programmation Fonctionnelle Avancée 7 : Inférence de types, polymorphie et traits impératifs Le typage en OCaml Programmation Fonctionnelle Avancée 7 : Inférence de types, polymorphie et traits impératifs Le typage en OCaml Exemple : résolution d'équations (4) Exemple d'échec de l'inférence de type I I Le mgu associe à tf le terme : f (f (t2 , f (p(f (tz , t2 ), tz ), t1 )), f (f (tz , t2 ), f (tz , t1 ))) I I C-à-d, dans la notion habituelle de OCaml, le type de f est : où, après renommage des variables (t2 7→ a, tz 7→ b , t1 7→ c ) : (a → (b → a) × b) → c) → (b → a) → b → c Regardons un deuxième exemple : l e t f g = ( g 4 2 ) && ( g " c o o c o o " ) ((t2 → (tz → t2 ) × tz ) → t1 ) → (tz → t2 ) → tz → t1 I Après élimination de tx : I Système d'équations : tf = f (tg , bool) tg = f (int, bool) tg = f (string, bool) On obtient avec les règles d'unication : f (int, bool) = f (string, bool) int = string Programmation Fonctionnelle Avancée 7 : Inférence de types, polymorphie et traits impératifs Le typage en OCaml Le typage en OCaml Pourquoi cet echec ? Quantication de variables de types l e t f g = ( g 4 2 ) && ( g " c o o c o o " ) I Programmation Fonctionnelle Avancée 7 : Inférence de types, polymorphie et traits impératifs I Pourquoi est-ce que g ne peut pas être du type :'a → bool ? I Pour voir la réponse il faut expliciter les quanticateur des variables de types. I Toutes les variables dans un type sont quantiées universellement au début du type : I Par exemple : Un type comme a → b → a × (b → a) est à lire comme ∀a, b : I Programmation Fonctionnelle Avancée 7 : Inférence de types, polymorphie et traits impératifs Le typage en OCaml Quantication de variables de types I a → bool) → bool Des types avec des ∀ sous une èche n'existent pas en OCaml, l'inférence a raison de refuser cette dénition. Ce qui existe en OCaml est le type ∀a : ((a → bool) → bool) mais c'est un type diérent ! Normalement, dans tous les types les variables de types sont (implicitement) quantiées ∀, avec un quanticateur devant le type complèt. Le type qu'on essaye de construire ici est : (∀a : I Programmation Fonctionnelle Avancée 7 : Inférence de types, polymorphie et traits impératifs Reprenons l'exemple : l e t f g = ( g 4 2 ) && ( g " c o o c o o " ) I Les variables a, b peuvent être instanciées par des types quelconques. Le typage en OCaml Quantication de variables de types I a → b → a × (b → a) I Normalement, les variables de types libres (non quantiées) paraissent seulement pendant la résolution des équations, une fois le type le plus général obtenu les variables sont quantiées. I I Nous verrons une exception à cette règle un peu plus tard. Dans un type dérivé pour f dans let f x1 ... xn =e, toutes les nouvelles variables de type sont quantiées au début par ∀. Programmation Fonctionnelle Avancée 7 : Inférence de types, polymorphie et traits impératifs Le typage en OCaml Programmation Fonctionnelle Avancée 7 : Inférence de types, polymorphie et traits impératifs Le typage en OCaml Example : Quantication de variables de type (1) Example : Quantication de variables de type (2) t2 I z }| { let f g h = fun x → h (g x) | {z } t | {z } I La solution trouvée pour la variable tf est : 3 tf = (tx → t3 ) → (t3 → t2 ) → (tx → t2 ) t1 I On obtient le système d'équations : tf = tg → (th → t1 ) t1 = tx → t2 I Dans ce type, les variables tx , t2 , t3 sont libres, elles sont donc implicitement quantiées avec un ∀ I Le type obtenu pour f est donc à lire comme : ∀tx , t3 , t2 : (tx → t3 ) → (t3 → t2 ) → (tx → t2 ) th = t3 → t2 tg = tx → t3 Programmation Fonctionnelle Avancée 7 : Inférence de types, polymorphie et traits impératifs Le typage en OCaml Quelques résultats fondamentaux I I Il existe un algorithme qui, étant donné une expression e, trouve, si elle est typable, son type σ le plus général possible, aussi appelé type principal Le premier algorithme pour cela est le W de Damas et Milner, qu'on trouve dans Principal type-schemes for functional programs. 9th Symposium on Principles of programming languages (POPL'82). I Cet algorithme utilise de façon essentielle l'algorithme d'unication de Herbrand/Robinson. I Les algorithmes modernes utilisent plutôt directement la résolution de contraintes. I À la surprise générale, en 1990 on a montré que l'inférence de type pour le noyau de ML est DEXPTIME complète. Programmation Fonctionnelle Avancée 7 : Inférence de types, polymorphie et traits impératifs Le typage en OCaml Exemples (inf4.ml) l e t p x y = fun z −> z x y ; ; let let let let let let let let x7 ; ; x0 x1 x2 x3 x4 x5 x6 x7 = = = = = = = = fun x −> x i n x0 x0 i n x1 x1 i n x2 x2 i n x3 x3 i n x4 x4 i n x5 x5 i n x6 x6 i n p p p p p p p Programmation Fonctionnelle Avancée 7 : Inférence de types, polymorphie et traits impératifs Le typage en OCaml Typage et traits impératifs Comment faire exploser le typeur d'OCaml... I Programmation Fonctionnelle Avancée 7 : Inférence de types, polymorphie et traits impératifs Dans l'exemple du transparent précédent : le type de xn a taille 2n ! Les règles de typage pour OCaml : ... I Standard : sommes tuples I enregistrements Plus compliqué : value restriction I typage des eets de bord (ce chapitre) I I I I let let let let let = fun = fun = fun = fun = fun f 4 ( fun z −> f0 f1 f2 f3 f4 x −> y −> y −> y −> y −> z );; (x , x) f0 ( f0 f1 ( f1 f2 ( f2 f3 ( f3 in y) y) y) y) I in in in in Heureusement, en pratique, personne n'écrit de code comme ça. L'inférence de type à la ML reste un des système de type les plus puissants. Programmation Fonctionnelle Avancée 7 : Inférence de types, polymorphie et traits impératifs Typage et traits impératifs La value restriction I En OCaml, on dispose de structures mutables capables de contenir des données de tout type. I Opérateurs pour les références : ref ∀a : a → (a ref) créer une référence vers une valeur ! ∀a : (a ref) → a déréférencer := ∀a : (a ref) → a → unit changer la valeur d'une case mémoire référencée On peut imaginer que ref est un constructeur de type (au même titre que →, ×, list, etc.) I I Essayons d'appliquer notre algorithme d'inférence de types en présence de références. I I Avancé : I I I I I modules récursion polymorphe objets variants polymorphes (voir la semaine prochaine) GADT (voir dans deux semaines) Programmation Fonctionnelle Avancée 7 : Inférence de types, polymorphie et traits impératifs Typage et traits impératifs Exemples (inf6.ml) l e t c = r e f ( f u n c t i o n x −> x ) ; ; c := ( f u n c t i o n x −> x + 1 ) ; ; ! c true ; ; Programmation Fonctionnelle Avancée 7 : Inférence de types, polymorphie et traits impératifs Typage et traits impératifs Typage et traits impératifs Inférence de types en présence de références I Selon notre algorithme, ref (function x -> x) a le type (a → a) ref I Programmation Fonctionnelle Avancée 7 : Inférence de types, polymorphie et traits impératifs Donc, on obtient pour c le type : ∀a : (a → a) ref I Le quanticateur universel pour a va permettre d'instancier a une fois par int, et puis par bool. I Évidement OCaml a raison de refuser ce code, il y a donc un problème avec notre inférence de types. Programmation Fonctionnelle Avancée 7 : Inférence de types, polymorphie et traits impératifs Typage et traits impératifs Inférence de type en présence de référence I Quelles sont les conditions qui permettent de quantier les variables de type ? I Une première idée est : l'expression ne contient pas du tout de références. I Ca marche, mais a une conséquence assez grave : le polymorphisme est eectivement desactivé dès qu'on utilise des références dans une fonction, même si l'utilisation est parfaitement sûre. I Regardons l'exemple suivant : Restriction de quantication I Le malheur vient du fait que la variable 'a est quantiée avec un ∀. I En vérité, une fois la variable 'a instanciée, on ne devrait plus avoir le droit de changer cette instanciation. I En présence de traits impératifs, on ne peut donc pas quantier les variables dans les types comme avant. I Idée : les variables de types sont quantiées seulement quand l'expression à la droite du let satisfait certaines conditions, sinon la variable reste libre et peu donc être instanciée une seule fois. I Ces conditions restent à déterminer ! I OCaml ache une variable de type libre comme '_a. Programmation Fonctionnelle Avancée 7 : Inférence de types, polymorphie et traits impératifs Typage et traits impératifs Exemples (value-restriction3.ml) l e t f a s t r e v = f u n c t i o n l i s t −> let l e f t = ref l i s t and r i g h t = r e f [ ] i n begin w h i l e ! l e f t <> [ ] do r i g h t := ( L i s t . hd ( ! l e f t ) ) : : ! r i g h t ; l e f t := L i s t . t l ( ! l e f t ) done ; ! right end ; ; ( ∗ OK ! ∗ ) fastrev [1;2;3;4];; f a s t r e v [ true ; true ; f a l s e ; f a l s e ] ; ; Programmation Fonctionnelle Avancée 7 : Inférence de types, polymorphie et traits impératifs Typage et traits impératifs Inférence de type en présence de référence I La question est alors : trouver la bonne condition sous laquelle les variables de type peuvent être quantiées, tel que : I I I I les erreurs de type pendant l'excution du programme sont excluses ; les fonctions polymorphes utilisant les références de façon sûre restent autorisées. Cette question a donné lieu à plusieurs propositions, toutes assez complexes. Une solution simple a été trouvée par Andrew K. Wright en 1995. Programmation Fonctionnelle Avancée 7 : Inférence de types, polymorphie et traits impératifs Typage et traits impératifs Exemples (value-restriction1.ml) l e t c = r e f ( f u n c t i o n x −> x ) ; ; ( ∗ t y p e o f c : ( ' _a −> '_a) r e f , '_a i s a f r e e v a r i a b l e ! ∗ ) c := ( f u n c t i o n x −> x + 1 ) ; ; c ;; ( ∗ t y p e o f c : ( i n t −> i n t ) r e f , '_a has been i n s t a n c i a t e d ∗ ) ! c true ; ; ( ∗ t y p e e r r o r : c l a s h between i n t and b o o l ∗ ) Programmation Fonctionnelle Avancée 7 : Inférence de types, polymorphie et traits impératifs Typage et traits impératifs La Value Restriction Solution simple introduite par SML : permettre la généralisation seulement pour les valeurs (d'où le nom). Les valeurs sont : I les constantes (13, "foo", 13.0, ...) I les variables (x, y, ...) I les fonctions (fun x -> e) I les constructeurs appliqués à des valeurs (Foo v), excepté ref I une valeur avec une contrainte de type (v : t) I un n-uplet de valeurs (v1, v2, ...) I un enregistrement contenant seulement des valeurs {l1 = v1, l2 = v2, ...} I une liste de valeurs [v1, v2, ...] Programmation Fonctionnelle Avancée 7 : Inférence de types, polymorphie et traits impératifs Typage et traits impératifs Conséquences de la Value Restriction I Dans l'exemple précédent, c a le type (' _a −> '_a) ref I Explication : l'expression n'est pas une valeur, donc il n'y a pas de généralisation (∀) lors du let. I Inconvénient : il y a parfois des programmes correctes qui sont refusés, comme sur l'exemple suivant. I On peut normalement contourner le problème facilement. I OCaml utilise une solution légèrement plus générale, due à Jacques Garrigue. Programmation Fonctionnelle Avancée 7 : Inférence de types, polymorphie et traits impératifs Typage et traits impératifs Exemples (value-restriction2.ml) l e t i d = f u n c t i o n x −> x ; ; (∗ type e r r o r ∗) let f = id id ; ; f 42;; f " coocoo " ; ; ( ∗ s o l u t i o n : eta − e x p a n s i o n ∗ ) l e t g = f u n c t i o n x −> ( i d i d ) x ; ; g 42;; g " coocoo " ; ; Programmation Fonctionnelle Avancée 7 : Inférence de types, polymorphie et traits impératifs Typage et traits impératifs Pour en savoir plus Harry G. Mairson. Deciding ML typability is complete for deterministic exponential time. In Proceedings of the 17th ACM SIGPLAN-SIGACT symposium on Principles of programming languages, POPL '90, pages 382401, New York, NY, USA, 1990. ACM. Andrew K. Wright. Simple imperative polymorphism. Lisp Symb. Comput., 8(4) :343355, December 1995. Jacques Garrigue. Relaxing the value restriction. In FLOPS 2004, pages 196213.