Fonctions ML des listes polymorphes aux listes polymorphes

8

J'apprends la programmation en ML (OCaml), et plus tôt j'ai posé des questions sur les fonctions ML de type'a -> 'b . Maintenant, j'expérimente un peu avec les fonctions de type 'a list -> 'b list. Il y a quelques exemples simples évidents:

let rec loop l = loop l
let return_empty l = []
let rec loop_if_not_empty = function [] -> []
                                   | l -> loop_if_not_empty l

Ce que je ne peux pas comprendre, c'est comment créer une fonction qui fait autre chose que retourner la liste ou la boucle vide (sans utiliser de fonction de bibliothèque). Cela peut-il être fait? Existe-t-il un moyen de renvoyer des listes non vides?

Edit: Oui, si j'ai une fonction de type 'a -> 'b, alors je peux en faire une autre, ou une fonction de type 'a list -> 'b list, mais ce que je me demande ici, c'est comment faire la première.

Gilles 'SO- arrête d'être méchant'
la source
1
Comme pour la question précédente, veuillez cibler la programmation d'apprentissage des élèves CS101 dans votre réponse, pas le théoricien du type que votre réponse pourrait l'inspirer à devenir plus tard.
Gilles 'SO- arrête d'être méchant'
Notez que si vous aviez une fonction f avec ce type renvoyant une liste non vide, alors fun a -> List.hd (f [a]) aurait le type 'a ->' b sans être sans fin ni lever une exception.
gallais

Réponses:

6

Eh bien, quelque chose connu sous le nom de paramétricité nous dit que si nous considérons le sous-ensemble pur de ML (c'est-à-dire pas de récursion infinie, refet toutes ces choses étranges), il n'y a aucun moyen de définir une fonction avec ce type autre que celle renvoyant le vide liste.

Tout a commencé avec l'article de Wadler « Theorems for free! ". Cet article, en gros, nous dit deux choses:

  1. Si nous considérons des langages de programmation qui remplissent certaines conditions, nous pouvons en déduire quelques théorèmes sympas simplement en regardant la signature de type d'une fonction polymorphe (c'est ce qu'on appelle le théorème de paramétrie).
  2. ML (sans récursion infinie refet tout ce truc bizarre) remplit ces conditions.

De l'paramétricité Théorème nous savons que si nous avons une fonction f : 'a list -> 'b list, puis pour tous 'a, 'b, 'c, 'det pour toutes les fonctions g : 'a -> 'c, h : 'b -> 'dnous avons:

map h ∘ f = f ∘ map g

(Remarque, fà gauche a le type 'a list -> 'b listet fà droite est 'c list -> 'd list.)

Nous sommes libres de choisir ce que gnous voulons, alors laissez 'a = 'cet g = id. Or depuis map id = id(facile à prouver par récurrence sur la définition de map), on a:

map h ∘ f = f

Maintenant, laissez 'b = 'd = boolet h = not. Supposons que pour certains, zs : bool listcela arrive f zs ≠ [] : bool list. Il est clair que map not ∘ f = fcela ne tient pas , car

(map not ∘ f) zs ≠ f zs

Si le premier élément de la liste à droite est true, alors à gauche le premier élément est falseet vice versa!

Cela signifie que notre hypothèse est fausse et f zs = []. Avons-nous fini? Non.

Nous avons supposé que 'best bool. Nous avons montré que lorsque fest invoqué avec type f : 'a list -> bool listpour any 'a, fdoit toujours renvoyer la liste vide. Se peut-il que lorsque nous appelons f, f : 'a list -> unit listcela renvoie quelque chose de différent? Notre intuition nous dit que c'est un non-sens: nous ne pouvons tout simplement pas écrire en ML pur une fonction qui retourne toujours la liste vide quand nous voulons qu'elle nous donne une liste de booléens et pourrait retourner une liste non vide sinon! Mais ce n'est pas une preuve.

Ce que nous voulons dire, c'est que fc'est uniforme : s'il retourne toujours la liste vide pour bool list, alors il doit retourner la liste vide pour unit listet, en général, tout 'a list. C'est exactement le sujet du deuxième point de la liste à puces au début de ma réponse.

Le papier nous dit qu'en ML fdoit prendre des valeurs liées à celles qui sont liées . Je n'entre pas dans les détails des relations, il suffit de dire que les listes sont liées si et seulement si elles ont des longueurs égales et leurs éléments sont liés par paires (c'est-à-dire, [x_1, x_2, ..., x_m]et [y_1, y_2, ..., y_n]sont liés si et seulement si m = net x_1sont liés à y_1et x_2est lié à y_2et ainsi de suite). Et la partie amusante est, dans notre cas, puisqu'elle fest polymorphe, on peut définir n'importe quelle relation sur les éléments des listes!

Choisissons 'a-en 'bet regardons f : 'a list -> 'b list. Regardez maintenant f : 'a list -> bool list; nous avons déjà montré que dans ce cas fretourne toujours la liste vide. Nous postulons maintenant que tous les éléments de 'asont liés à eux-mêmes (rappelez-vous, nous pouvons choisir n'importe quelle relation que nous voulons), cela implique que tout zs : 'a listest lié à lui-même. Comme nous le savons, fprend des valeurs liées à celles liées, cela signifie que f zs : 'b listest lié à f zs : bool list, mais la deuxième liste a une longueur égale à zéro, et puisque la première est liée à elle, elle est également vide.


Pour être complet, je mentionnerai qu'il y a une section sur l'impact de la récursivité générale (non-résiliation possible) dans l'article original de Wadler, et il y a aussi un article explorant les théorèmes libres en présence de seq.

Kirelagin
la source
Maintenant, je soupçonne que la preuve peut être faite en une seule étape si au lieu d'affaiblir le théorème de paramétrie en considérant des relations spécifiques induites par des fonctions ( get hdans ce cas) aller directement avec des relations générales sur mesure…
kirelagin
Nitpick, la paramétricité ne commence pas avec l'article de Wadler (qui prétend être un résumé des approches pour définir la paramétricité). L'idée remonte à l'article de Reynold «Types, Abstraction and Parametric Polymorphism». L'idée était également un peu présente dans la preuve de normalisation de Girard pour le système F pour autant que je sache.
Daniel Gratzer
4

Revenons à des objets plus simples: vous ne pouvez pas construire un objet de type approprié 'acar cela signifierait que cet objet xpeut être utilisé partout où il 'aconviendrait. Et cela signifie partout: comme un entier, un tableau, même une fonction. Par exemple, cela signifierait que vous pouvez faire des choses comme x+2, x.(1)et (x 5). Les types existent exactement pour éviter cela.

C'est la même idée qui s'applique à une fonction de type 'a -> 'b, mais il existe des cas où ce type peut exister: lorsque la fonction ne retourne jamais un objet de type 'b: lors de la boucle ou de la levée d'une exception.

Cela s'applique également aux fonctions qui renvoient une liste. Si votre fonction est de type t -> 'b listet que vous générez un objet de type tet l'appliquez à cette fonction, cela signifie que si vous accédez avec succès à un élément de cette liste, vous accéderez à un objet de tous types. Vous ne pouvez donc accéder à aucun élément de la liste: la liste est vide ou ... il n'y a pas de liste.

Cependant, le type 'a list -> 'b listapparaît dans les exercices habituels, mais ce n'est que lorsque vous avez déjà une fonction de type 'a -> 'b:

let rec map (f : 'a -> 'b) =
  function
  | [] -> []
  | x :: xs -> f x :: map f xs

Mais vous connaissez probablement celui-ci.

val map : ('a -> 'b) -> 'a list -> 'b list
jmad
la source
1
Le théoricien de type plus ancien n'est pas ravi de cette réponse. Ok, un contexte de variable de type non vide est un moyen d'avoir une fonction qui est littéralement de type 'a -> 'bou 'a list -> 'b list, mais ce n'est pas une observation si intéressante. En fait, je vais modifier la question pour bien faire comprendre que ce n'est pas ce que les jeunes élèves apprennent à propos des programmes d'apprentissage.
Gilles 'SO- arrête d'être méchant'
Mais le théoricien de type plus ancien sait que ML n'est pas logiquement défectueux. Si vous pouvez produire une fonction f : 'a list -> 'b listet de ttelle sorte que , f t <> []alors ce programme tapera-chèque , mais peut faire façon pire que soulever une exception: let y = List.hd (f t) in (y y) (y + y.(0) + y.(0).(0)).
jmad
2

Théorème de paramétrie des «Théorèmes gratuits!» le papier nous dit que les termes ML ont une propriété très spéciale: si nous considérons le type d'un terme comme une relation sur des valeurs de ce type, alors la valeur de ce terme sera liée à lui-même. Voici comment afficher les types en tant que relations:

  • Un type de fonction 'a -> 'bcorrespond à la relation définie en disant que deux fonctions sont liées si elles prennent des valeurs liées à des valeurs liées (en supposant 'aet 'bcorrespondant à certaines relations).
  • Un type de liste 'a listcorrespond à la relation définie en disant que deux listes sont liées si elles ont la même longueur et si leurs éléments correspondants sont liés (en supposant que cela 'acorrespond à une relation).
  • (Maintenant , la partie la plus intéressante.) A de correspond de type polymorphes à la relation définie en disant que deux valeurs polymorphes sont liées si nous pouvons choisir les deux types, une relation entre les éléments de ces types, remplacer toutes les occurrences de la variable de type avec ce et les valeurs résultantes seront toujours liées.

Voici un exemple. Supposons que nous ayons un terme foo : 'a -> 'a. Le théorème de paramétrie dit que cela fooest lié à lui-même. Cela signifie que nous pouvons choisir deux types, par exemple,UNE1 et UNE2, choisissez absolument n'importe quelle relation UNE entre les éléments de ce type, et si nous prenons tout une1:UNE1 et une2:UNE2, tels qu'ils sont liés selon UNE, puis fooune1 et fooune2 sera également lié selon UNE:

une1UNEune2fooune1UNEfooune2.

Maintenant, si nous prenons la relation UNE ne pas être une relation arbitraire, mais une fonction F:UNE1UNE2, ce qui précède devient:

F(une1)=une2F(fooune1)=fooune2,

ou, en d'autres termes:

F(fooune1)=foo(F(une1)),

ce qui est exactement le théorème libre pour la idfonction: f . id = id . f.


Si vous effectuez des étapes similaires pour votre fonction foo : 'a list -> 'b list, vous obtiendrez que vous pouvez choisir deux typesUNE1 et UNE2, toute relation UNE entre leurs éléments, deux types quelconques B1 et B2, toute relation B entre leurs éléments, puis prendre deux listes quelconques, d’abord constituées d’éléments de UNE1, deuxième composé d'éléments de UNE2, appliquez votre fonction aux deux listes (obtenir une liste de B1 dans le premier cas et une liste de B2 dans le second), et les résultats seront liés, si les entrées étaient liées.

Maintenant, nous utilisons cela pour prouver que pour deux types quelconques Aet Bla fonction foorenvoie une liste vide pour toute entrée as : A list.

  • Laisser UNE1=UNE2= A et laisse UNE être la relation d'identité, donc toute liste de UNE est trivialement lié à lui-même.
  • Laisser B1= Ø, B2= B et B toute relation entre eux (il n'y en a qu'une, la vide, mais ça n'a pas d'importance).
  • asest lié à lui-même (comme nous avons choisi la relation d'identité A), foo as : Ø listest donc lié à foo as : B list.
  • Nous savons que deux listes ne peuvent être liées que si leurs longueurs sont égales et nous savons également que la première des listes résultantes doit être vide, car il ne peut y avoir d'éléments du Øtype.

Par conséquent, pour tout A, Bet as : A listnous avons que cela foo as : B listdoit être une liste vide.

Kirelagin
la source