AccueilđŸ‡«đŸ‡·Chercher

Type récursif

En programmation informatique et thĂ©orie des types, un type rĂ©cursif est un type de donnĂ©es dont la dĂ©finition fait appel au type lui‐mĂȘme, de façon rĂ©cursive. Cela permet entre autres des structures de donnĂ©es qui contiennent des sous‐structures du mĂȘme type. Cette notion s'applique naturellement dans l'Ă©tude des listes et des arbres.

Exemples

Types algébriques

Les types algébriques sont de loin la forme la plus courante de types récursifs. Un exemple classique est le type liste. En syntaxe Haskell (avec une variable de type a qui rend cette définition polymorphe) :

data List a = Nil | Cons a (List a)

Autrement dit, une liste d’élĂ©ments de type a est soit une liste vide, soit une cellule contenant une donnĂ©e de type a (la « tĂȘte » de liste) et une autre liste (la « queue » de liste).

Autre exemple, le type des entiers naturels (voir l’arithmĂ©tique de PĂ©ano) peut ĂȘtre dĂ©fini par :

  data Nat = Zero | Succ Nat

c’est‐à‐dire qu’un entier naturel est soit zĂ©ro, soit le successeur d’un entier naturel.

Types récursifs de fonctions

On peut avoir besoin d’une fonction qui se prend elle‐mĂȘme en argument. Par exemple, si l’on souhaite Ă©crire le combinateur de point fixe Y, qui permet de dĂ©finir des fonctions rĂ©cursives[1] :

-- ne compile pas !
fix :: ((a -> b) -> a -> b) -> a -> b
fix f = let g x a = f (x x) a in g g

alors, bien que fix n’ait pas lui‐mĂȘme un type rĂ©cursif, sa dĂ©finition en fait intervenir un. En effet, la variable x est appliquĂ©e Ă  elle‐mĂȘme donc, si l’on note son type, alors x est aussi une fonction prenant un argument de type . On obtient alors un type rĂ©cursif dĂ©fini par l’équation pour un certain type [note 1].

De façon symĂ©trique, il est parfois utile d’écrire une fonction qui se renvoie elle‐mĂȘme, ou une autre fonction du mĂȘme type (par exemple pour coder un automate ou un gestionnaire d’évĂ©nements)[C 1] - [C 2]. Un exemple simplifiĂ© en langage C :

/* syntaxe C pour l’équation de types « function_t = (int → function_t) » ;
 * ne compile pas ! */
typedef function_t (*function_t) (int) ;
function_t f (int n)
{
	printf("%i", n) ;
	return f ;
}
int main (void)
{
	f(1)(2)(3) ; /* devrait Ă©crire « 123 » */
	return 0 ;
}

Si l’on note le type de la fonction f, alors f est une fonction qui prend un entier (type ) et se renvoie elle‐mĂȘme (type ) ; son type est donc aussi , d’oĂč l’équation .

En fait, comme expliquĂ© plus loin, de tels types rĂ©cursifs ne sont pas permis tels quels par les langages C ni Haskell. Ces exemples fonctionnent en langage OCaml avec l’option de compilation -rectypes[3] :

(* fix : (('a -> 'b) -> 'a -> 'b) -> 'a -> 'b ;
 * requiert l’option de compilation « -rectypes » *)
let fix f = let g x a = f (x x) a in g g
(* f : (int -> 'a) as 'a ;
 * requiert l’option de compilation « -rectypes » *)
let rec f n =
  Format.printf "%i" n ;
  f
let _ =
  f 1 2 3 (* Ă©crit « 123 » *)

Théorie

En thĂ©orie des types, un type rĂ©cursif a la forme . Intuitivement, cette notation dĂ©signe le type reprĂ©sentĂ© par l’expression , dans lequel la variable de type peut apparaĂźtre et dĂ©signe le type lui‐mĂȘme[note 2].

Par exemple, si , alors est le type d’une fonction qui prend un entier et renvoie une fonction de mĂȘme type qu’elle‐mĂȘme. On peut dĂ©rouler cette expression de type, en remplaçant dans la variable par le type (ce qu’on note ), ce qui donne . On peut itĂ©rer cette opĂ©ration autant de fois que souhaitĂ© :

L’exemple ci‐dessus des entiers naturels s’écrit , oĂč les deux bras du type somme reprĂ©sentent les constructeurs Zero et Succ, avec Zero ne prenant pas d'argument (reprĂ©sentĂ© par le type unitĂ©) et Succ prenant un Ă©lĂ©ment du type Nat lui‐mĂȘme (reprĂ©sentĂ© par la variable de type ).

L’exemple des listes d’élĂ©ments de type , quant Ă  lui, s’écrit . Le type unitĂ© reprĂ©sente le constructeur Nil sans argument, le type produit reprĂ©sente le constructeur Cons ayant pour arguments une valeur de type et une autre valeur de type . En dĂ©roulant :

, soit
, soit
, soit

On obtient successivement le type des listes de longueur zéro (), un (), deux (), trois (), et ainsi de suite.

L’idĂ©e est que le type rĂ©cursif vĂ©rifie l’équation informelle suivante :

Dans les exemples prĂ©cĂ©dents, elle s’écrit respectivement :

Si l’on s’autorisait les expressions de type infinies, on pourrait dĂ©rouler Ă  l’infini et Ă©crire, respectivement :

  • (une fonction curryfiĂ©e acceptant autant d’arguments de type que souhaitĂ©)
  • (c’est en effet une union disjointe de singletons que l’on peut nommer , , , etc.)
  • (l’étoile de Kleene)

Ces expressions infinies sont cependant mal dĂ©finies, car elles ne reflĂštent qu’un seul type solution alors que l’équation de types peut en admettre plusieurs. On rĂ©sout l’ambigĂŒitĂ© en choisissant le plus petit ou le plus grand point fixe du dĂ©roulement, comme expliquĂ© dans la section « DonnĂ©es & co‐donnĂ©es ».

Pour donner un sens Ă  cette Ă©galitĂ© de types, on distingue deux formes de rĂ©cursivitĂ© qui diffĂšrent dans la maniĂšre d’introduire et d’éliminer les types rĂ©cursifs : l’équirĂ©cursivitĂ© et l’isorĂ©cursivitĂ©.

Types équirécursifs

Avec des types Ă©quirĂ©cursifs, un type et son dĂ©roulement sont Ă©gaux — c'est-Ă -dire que ces deux expressions de type dĂ©notent le mĂȘme type. En fait, la plupart des thĂ©ories de types Ă©quirĂ©cursifs vont plus loin et Ă©noncent que deux expressions avec le mĂȘme « dĂ©roulement infini » sont Ă©quivalentes. Cela complique le systĂšme de types, bien plus qu’avec des types isorĂ©cursifs. Les problĂšmes algorithmiques tels que la vĂ©rification des types ou l'infĂ©rence de types sont aussi plus difficiles. Comme la simple comparaison syntaxique n’est pas suffisante pour les types Ă©quirĂ©cursifs, il faut les convertir en une forme canonique, ce qui peut se faire en [4].

Les types équirécursifs capturent le caractÚre auto-référent (ou mutuellement référent) des définitions de types dans les langages procéduraux et orientés objet. Ils surviennent aussi dans la sémantique des objets et classes en théorie des types.

Types isorécursifs

Avec des types isorécursifs, un type et son déroulement sont des types distincts et disjoints, néanmoins reliés par un isomorphisme qui se matérialise par des constructions de termes spécifiques. Plus précisément, on dispose de fonctions inverses et .

Les types isorĂ©cursifs sont trĂšs prĂ©sents dans les langages fonctionnels sous la forme de types algĂ©briques. De fait, les langages Haskell et OCaml (sauf avec l’option de compilation -rectypes)[3] interdisent la rĂ©cursion dans les alias de type — c’est‐à‐dire l’équirĂ©cursivitĂ©. Ainsi, les dĂ©finitions suivantes sont illĂ©gales en Haskell :

type Bad = (Int, Bad)
type Evil = Bool -> Evil

Cette limitation existe car, Ă  l’instar de typedef en langage C et contrairement aux types algĂ©briques, les alias de types sont simplement remplacĂ©s par leur dĂ©finition lors de la compilation. Les types algĂ©briques permettent de contourner cette limitation en enveloppant l’expression rĂ©cursive dans un constructeur :

data Good = Pair Int Good
data Fine = Fun (Bool->Fine)

Une façon de le comprendre est de remarquer que ce niveau d’indirection supplĂ©mentaire explicite les opĂ©rations d’enroulement et de dĂ©roulement du type : la construction d’une valeur (l’application d’un constructeur) enroule son type, tandis que sa destruction (par filtrage par motif) le dĂ©roule. Dans le dernier exemple :

roll :: (Bool->Fine) -> Fine
roll x = Fun x
unroll :: Fine -> (Bool->Fine)
unroll (Fun x) = x

Ceci permet d’écrire en Haskell le combinateur de point fixe vu plus haut[1] :

data FunctionTakingItself b = R (FunctionTakingItself b -> b)
fix :: ((a -> b) -> a -> b) -> a -> b
fix f = let g y@(R x) a = f (x y) a in g (R g)

De mĂȘme, on peut Ă©crire en langage C l’exemple de fonction qui se renvoie elle‐mĂȘme[C 1] - [C 2] :

/* définition récursive du type encapsulée dans une structure */
struct function_t {
	struct function_t (*fun) (int) ;
};
typedef struct function_t function_t ;
/* définition mutuellement récursive de la fonction f et de son encapsulation g */
static function_t g ;
function_t f (int n)
{
	printf("%i", n) ;
	return g ;
}
static function_t g = { .fun = f } ;
int main (void)
{
	g.fun(1).fun(2).fun(3) ; /* Ă©crit « 123 » */
	return 0 ;
}

DonnĂ©es & co‐donnĂ©es


AlgĂ©briquement, on peut dĂ©finir le type comme un point fixe de la fonction (qui Ă  un type associe un type). La question est de choisir ce point fixe. On peut prendre soit le plus petit point fixe, soit le plus grand. On parle parfois de donnĂ©es dans le premier cas et de co‐donnĂ©es dans le second cas. Cette distinction est particuliĂšrement importante en programmation totale (en), oĂč l’on veut garantir la terminaison de tous les calculs[2]. En effet, dans le cas des types algĂ©briques :

  • les donnĂ©es sont bien fondĂ©es et prĂ©sentent donc un principe de rĂ©currence, qui permet de les dĂ©truire (dĂ©finir des fonctions sur ces donnĂ©es) rĂ©cursivement de façon terminante ;
  • les co‐donnĂ©es prĂ©sentent un principe de co-rĂ©currence, qui permet de les construire (dĂ©finir de telles valeurs) rĂ©cursivement de façon productive.

Ces deux notions sont duales.

Dans un langage total, ce sont les deux seules formes autorisées de récursivité. Ces deux restrictions visent à garantir un bon comportement des programmes.

  • Que les donnĂ©es soient nĂ©cessairement finies assure la terminaison des fonctions qui les traitent, grĂące au principe de rĂ©currence.
  • L’introduction des co‐donnĂ©es potentiellement infinies permet de retrouver une forme de non‐terminaison nĂ©cessaire pour rĂ©aliser des programmes Ă  boucle d’ïnteraction, tels qu’un systĂšme d’exploitation, qui rĂ©agissent Ă  un flux d’évĂ©nements extĂ©rieurs. Ici, le bon comportement est garanti par la productivitĂ© des dĂ©finitions co‐rĂ©cursives : chaque Ă©tape de calcul produit une portion (un co‐constructeur) de co‐donnĂ©e, qui est donc exploitable par la suite.

Afin de ne pas risquer qu’une fonction dĂ©finie par rĂ©currence sur une donnĂ©e soit appelĂ©e sur une co‐donnĂ©e infinie, on considĂ©rera distincts les types de donnĂ©es et les types de co‐donnĂ©es, avec deux mots‐clĂ©s diffĂ©rents pour les dĂ©finir.


Dans l’exemple prĂ©cĂ©dent des listes, la fonction de types Ă©tait .

  • Son plus petit point fixe est , c’est‐à‐dire le type des listes finies.
  • Son plus grand point fixe est , c’est‐à‐dire l’ensemble des listes potentiellement infinies.

Si l’on n’avait pas le constructeur Nil (permettant de fonder une liste), alors la fonction serait et alors :

  • le plus petit point fixe est le type vide , ne contenant aucune valeur (sans Nil, il est impossible de construire une liste finie) ;
  • le plus grand point fixe est , le type des listes infinies.

De mĂȘme, dans l’exemple des entiers naturels, on considĂšre  :

  • le plus petit point fixe est , le type des entiers naturels ;
  • le plus grand point fixe est .

Et une remarque analogue s’applique pour le constructeur Zero.

Les langages Turing‐complets usuels ignorent cette nuance et permettent une rĂ©cursivitĂ© non contrĂŽlĂ©e. Il n’y a donc qu’un seul mot‐clĂ© pour dĂ©finir un type rĂ©cursif, qui est implicitement le plus grand point fixe. Ainsi en Haskell :

data List a = Nil | Cons a (List a)

on peut utiliser ces listes comme des donnĂ©es, avec une fonction dĂ©finie par rĂ©currence (cette dĂ©finition est bien fondĂ©e : chaque appel rĂ©cursif se fait sur des donnĂ©es structurellement plus petites) :

length :: List a -> Int
length Nil           = 0
length (Cons _ tail) = 1 + length tail

ou comme des co‐donnĂ©es, avec une dĂ©finition par co‐rĂ©currence (cette dĂ©finition est productive : chaque itĂ©ration produit un constructeur) :

infinite_list :: List Int
infinite_list = Cons 1 infinite_list -- la liste [ 1, 1, 1, 
 ]
naturals :: List Int
naturals = naturals_from 0 where -- la liste [ 0, 1, 2, 
 ]
  naturals_from n = Cons n (naturals_from (n+1))

L’évaluation paresseuse permet d’écrire cette dĂ©finition sans entrer dans une boucle infinie d’évaluation. On voit ainsi que la paresse est essentielle pour construire des co‐donnĂ©es dans un langage total.

Mais on peut Ă©galement Ă©crire des dĂ©finitions rĂ©cursives qui ne sont ni rĂ©currentes ni co‐rĂ©currentes, qu’elles soient pertinentes ou pas. Ainsi l’exemple prĂ©cĂ©dent peut se rĂ©Ă©crire de façon parfaitement valide suivant un idiome courant en Haskell (cette dĂ©finition n’est pas co‐rĂ©currente au sens le plus courant, Ă  cause de l’appel Ă  map) :

naturals :: List Int
naturals = Cons 0 (map succ naturals) -- la liste [ 0, 1, 2, 
 ]

Mais rien ne nous empĂȘche d’écrire des programmes boguĂ©s :

stupid list = stupid (Cons 1 list)
idiot = length naturals

Voir aussi

Notes

  1. La rĂ©cursivitĂ© de types contravariante, dont est un exemple, donne accĂšs Ă  la Turing‐complĂ©tude et Ă  la non‐terminaison. En effet, comme on vient de le voir, elle permet de typer le combinateur de point fixe Y grĂące auquel on peut Ă©crire toutes les fonctions rĂ©cursives. On peut aussi Ă©crire plus directement le terme non‐terminant (c’est‐à‐dire )[2].
  2. Comme dans la notation du lambda-calcul, la variable est liĂ©e dans l’expression ; la lettre n’est pas significative, elle fait partie de la syntaxe.

Références

  1. (en) « Type checking and recursive types (writing the Y combinator in Haskell/OCaml) », sur programmers.stackexchange.com, (consulté le )
  2. (en) David A. Turner, « Total Functional Programming », Journal of Universal Computer Science, vol. 10, no 7,‎ , p. 751–768 (DOI 10.3217/jucs-010-07-0751, lire en ligne [PDF])
  3. (en) « The OCaml system, § 6.4. Type expressions : Aliased and recursive types », sur caml.inria.fr (consultĂ© le ) : « Recursive types for which there exists a recursive path that does not contain an object or polymorphic variant type constructor are rejected, except when the -rectypes mode is selected. »
  4. (en) Nadji Gauthier et François Pottier, « Numbering Matters: First-Order Canonical Forms for Second-Order Recursive Types », Proceedings of the ACM SIGPLAN International Conference on Functional Programming (ICFP), vol. 39, no 9,‎ , p. 150-161 (DOI 10.1145/1016850.1016872, lire en ligne [PDF])

(en) Cet article contient des extraits de la Free On-line Dictionary of Computing qui autorise l'utilisation de son contenu sous licence GFDL.

Cet article est issu de wikipedia. Text licence: CC BY-SA 4.0, Des conditions supplĂ©mentaires peuvent s’appliquer aux fichiers multimĂ©dias.