Cours 3

Htmligure
Version & licenses
Creative Commons License

Transformer les listes

Guyslain
Thursday, 21 July 2016

Retour au sommaire.

Nous apprenons maintenant à manipuler des listes d'éléments, en utilisant des fonctions haut-niveau, transformant des listes entières en d'autres listes par un simple appel de fonction.

Il y a essentiellement trois types de fonctions de transformation de listes :

  • les fonctions prenant une liste et construisant une liste plus courte,
  • les fonctions prenant une liste et remplaçant chaque élément par une autre valeur (par persistence, ce n'est pas vraiment des remplacements, on obtient une nouvelle liste),
  • les fonctions prenant une liste et construisant une liste plus longue, en remplaçant chaque élément par plusieurs valeurs.

Chacun de ces trois types correspond à une fonctionnelle.

Supprimer des éléments d'une liste.

Commençons par la suppression d'éléments. Si on souhaite supprimer un élément particulier, il n'y a pas d'opération prédéfinie pour cela. La raison est simplement que les listes ne sont pas conçues pour supporter efficacement cette opération, supprimer un élément arbitraire est une opération demandant potentiellement de parcourir toute la liste, et donc n'est pas encouragé.

Ce qui est plus typique des opérations de listes est de supprimer tous les éléments qui ne nous satisfont pas. En passant à la négation on ne veut garder que les éléments qui vérifient une propriété, donnée de nouveau par un prédicat. On veut donc filtrer les éléments pour garder ceux qui vérifie un critère. La fonction correspondante s'appelle filter

  1. val List.filter : ('elt -> bool) -> 'elt list -> 'elt list
  2. val Core.Std.List.filter : 'elt list -> f:('elt -> bool) -> 'elt list


  3. let list0 = [1;2;3;4;5;6;7]
  4. let list1 = List.filter is_even list1 (* [2;4;6] *)
  5. let list2 = List.filter (fun i -> not (is_even i)) list1 (* [1;3;5;7] *)
  6. let empty = List.filter is_even [1;3;5;7]

On veut parfois utiliser à la fois les éléments vérifiant le critère, et pour un autre but les éléments ne vérifiant pas le critère. On peut filtrer deux fois la même liste, comme dans l'exemple pour obtenir list1 et list2. Mais cela oblige à faire deux fois le travail, on peut donc utiliser la fonction partition pour cela :

  1. val List.partition : ('elt -> bool) -> 'elt list -> ('elt list * 'elt list)
  2. val Core.Std.List.partition_tf :
  3. 'elt t -> f:('elt -> bool) -> 'elt t * 'elt t

  4. let (evens,odds) = List.partition is_even [1;2;3;4;5;6;7]

Notez qu'on peut utiliser filter pour supprimer un élément d'une liste :

  1. let remove elt list = List.filter ((<>) elt) list

Cela supprime toutes les occurences de elt de la liste, pas uniquement la première. Et le caractère linéaire de l'algorithme est clair puisque filter teste tous les éléments.

Modifier chaque élément d'une liste.

La deuxième transformation consiste à modifier chaque élément, indépendamment. Chaque élément est remplacée par une valeur, cette valeur dépendant uniquement de l'élément remplacé. Il nous faut donc une fonction, qui nous dit à chaque élément par quoi le remplacer. Nous avons déjà vu cette fonction dans le cadre des conteneurs :

  1. val List.map : ('elt -> 'im) -> 'elt list -> 'im list
  2. val Core.Std.List.map : 'elt list -> ~f:('elt -> 'im) -> 'im list

  3. let double n = 2 * n
  4. let res1 = List.map double [1;5;3;2] (* gives [2;10;6;4] *)
  5. let res2 = List.map ((+) 1) [1;5;3;2] (* gives [2;6;4;3] *)

Notez que l'ordre des images est conservé par rapport à l'ordre des éléments. Puisque chaque élément est individuellement transformé en un autre. La liste obtenue a la même longueur que la liste initiale.

La définition de map peut être donnée par cette équation :

  1. map f [e1;e2;e3;...;en] = [f e1; f e2; f e3; ... ; f en]

On peut reparenthéser le type de map ainsi :

  1. val map : ('elt -> 'im) -> ('elt list -> 'im list)

On peut maintenant interpréter map comme une fonction qui transforme une fonction sur les éléments en une fonction sur les listes d'éléments, tout comme nous l'avons fait pour exists et for_all. En ce sens map est un combinateur qui transforme une fonction en une fonction plus puissante.

Il existe une variante de map, lorsqu'on souhaite que l'image d'un élément dépendent aussi de sa position dans la liste. La fonction à appliquer prend alors deux arguments : l'entier représentant la position de l'élément dans la liste, et l'élément lui-même.

  1. val List.mapi : (int -> 'elt -> 'im) -> 'elt list -> 'im list
  2. val Core.Std.List.mapi : 'elt list -> f:(int -> 'elt -> 'im) -> 'im list

  3. let incr_even_pos_and_decr_odd_pos pos value =
  4. if is_even pos then value + 1
  5. else value - 1

  6. let res = List.mapi incr_even_pos_and_decr_odd_pos [1;3;2;4;7;6]
  7. (* res = [2;2;3;3;8;5] *)

Une application importante de cette fonction est d'indicer les éléments d'une liste, c'est-à-dire transformer chaque élément en une paire d'un élément et de son indice dans la liste.

  1. let indice list =
  2. List.mapi (fun i elt -> (i,elt)) list

  3. let res = indice [4;3;5;6;2;7;1]
  4. (* res = [ (0,4); (1,3); (2,5); (3,6); (4,2); (5,7); (6,1) ] *)

Une autre application classique est de faire le produit cartésien de deux listes Core.Std.List.cartesian_product, c'est-à-dire obtenir l'ensemble des paires formées d'un élément d'une première liste et d'un élément d'une deuxième liste.

  1. let almost_cartesian_product list1 list2 =
  2. List.map
  3. (fun elt1 ->
  4. List.map
  5. (fun elt2 -> (elt1, elt2))
  6. list2
  7. )
  8. list1

  9. let res = almost_cartesian_product [1;2;3] ['a';'b']
  10. (* res = [ [(1,'a');(1,'b')]; [(2,'a');(2,'b')]; [(3,'a');(3,'b')] ] *)

Ce code est assez peu compréhensible, et pas tout à fait correct puisqu'on obtient une liste de listes de paires, et non une liste de paires. Il faut comprendre que map fait un calcul pour tous les éléments d'une liste, c'est une sorte de boucle for. Le premier map ici fait varier elt1 sur la list1, le second fait varier elt2 sur list2, et on retourne toutes les paires (elt1,elt2).

On peut rendre le code plus lisible avec cette transformation raisonnable, consistant à nommer les fonctions anonymes.

  1. let almost_cartesian_product list1 list2 =
  2. let pairs_with elt1 elt2 = (elt1, elt2) in
  3. let pairs_with_list2 elt1 = List.map (pairs_with elt1) list2 in
  4. List.map pairs_with_list2 list1

ou bien on peut utiliser des astuces syntaxiques, compréhensibles uniquement par les experts (donc à prohiber, mais c'est rigolo néanmoins) :

  1. let almost_cartesian_product list1 list2 =
  2. list1 |> List.map @@ fun elt1 ->
  3. list2 |> List.map @@ fun elt2 ->
  4. (elt1, elt2)

Cette mauvaise solution suggère néanmoins l'existence d'une bonne solution. Il suffit d'introduire un opérateur binaire pour remplacer l'incompréhensible |> List.map @@. Cet opérateur est souvent noté >|=, mais dans Core.Std.List il est noté >>|, c'est simplement la notation infixe de map. Nous avons déjà vu map dans plusieurs contextes, et donc cet opérateur aussi est fréquent, et donc mérite d'être connu et utilisé. On obtient :

  1. let almost_cartesian_product list1 list2 =
  2. let open Core.Std.List in
  3. list1 >>| fun elt1 ->
  4. list2 >>| fun elt2 ->
  5. (elt1, elt2)

Il faut lire les lignes de droite à gauche : pour tout elt1 dans list1, pour tout elt2 dans list2, construire la paire (elt1, elt2).

Pour terminer le produit cartésien, il faudrait pouvoir agglutiner toutes les listes ensembles. Ceci nous amène aux fonctionnelles construisant des listes plus longues.

Combiner des listes.

L'opération de base pour combiner des listes est de faire l'union des deux listes. Techniquement on appelle cette opération une concaténation, car

  • les éléments sont dans le même ordre dans la liste obtenue que dans les listes originelles.
  • il ne s'agit pas d'une union ensembliste, au sens que le résultat peut contenir des valeurs présentes en plusieurs exemplaires.

Les deux listes concaténées doivent avoir le même type. Il n'y a pas de piège par ailleurs, et la fonction s'appelle append. On peut aussi utiliser l'opérateur infixe @.

  1. val List.append : 'elt list -> 'elt list -> 'elt list
  2. val (@) : 'elt list -> 'elt list -> 'elt list
  3. val Core.Std.List.append : 'elt list -> 'elt list -> 'elt list

  4. let res1 = [1;2;4] @ [3;5;6] (* res1 = [1;2;4;3;5;6] *)
  5. let res2 = [] @ [1;2;3] (* res2 = [1;2;3] *)
  6. let res3 = [1;2;3] @ [] (* res3 = [1;2;3] *)

La complexité de append est linéaire dans la longueur du premier argument : les éléments de la première liste sont ajoutés un par un à la deuxième liste. Du coup, @ est associatif à droite, ce qui optimise la complexité :

  1. list1 @ list2 @ list3 @ list4 = list1 @ (list2 @ (list3 @ list4))

Dans tous les cas, on évitera d'écrire :

  1. let add_last list elt = list @ [elt]

Cette opération prend un temps linéaire dans la longueur de la liste. Lorsqu'on veut utiliser une séquence en ajoutant des éléments en fin de séquence, la structure de données adaptée n'est pas la liste, mais la file à double extrémité.

rev_append est une version légèrement plus efficace de la concaténation (la complexité asymptotique est la même), mais qui renverse l'ordre des éléments de la première liste. Il arrive souvent que l'ordre des éléments n'a pas d'importance, et il arrive aussi régulièrement que la première liste soit de toute façon à l'envers. rev est la fonction qui renverse l'ordre des éléments d'une liste, rev_append est donc une composition de rev et append.

  1. val List.rev : 'elt list -> 'elt list
  2. val Core.Std.List.rev : 'elt list -> 'elt list

  3. val List.rev_append : 'elt list -> 'elt list -> 'elt list
  4. val Core.Std.List.rev_append : 'elt list -> 'elt list -> 'elt list

  5. let res1 = List.rev [1;2;3] (* res1 = [3;2;1] *)
  6. let res2 = List.rev [] (* res2 = [] *)

  7. let res3 = List.rev_append [3;2;1] [4;5;6] (* res3 = [1;2;3;4;5;6] *)
  8. let res4 = List.rev_append [5;2;4] [1;6;3] (* res4 = [4;2;5;1;6;3] *)

Nous avons aussi :

  1. [e1;e2;...;en] @ [f1;f2;...;fp] = [e1;e2;...;en;f1;f2;...;fp]

  2. List.rev_append [e1;e2;...;en] [f1;f2;...;fp] = [en;...;e2;e1;f1;f2;...;fp]

  3. List.rev_append list1 list2 = List.rev list1 @ list2

append combine deux listes en une seule. Pour combiner une liste de listes en une seule, comme on en a besoin pour finir le produit cartésien, on utilise la concaténation généralisée, qui se nomme flatten ou concat dans la librairie standard, et concat dans Core. Une liste de listes peut être interprétée comme une liste de lignes, donc comme un tableau bi-dimensionnel. Transformer un tableau en liste, c'est mettre les lignes à la suite les unes des autres, de manière à former une seule longue ligne. Le nom flatten vient de là : on aplatit un tableau en un liste.

  1. val List.concat : 'elt list list -> 'elt list
  2. val Core.Std.concat : 'elt list list -> 'elt list

  3. let res1 = List.concat [[1;2];[5];[6;3];[];[4;2]] (* = [1;2;5;6;3;4;2] *)
  4. let res2 = List.concat [[];[3];[3;4];[];[5;4;3]] (* = [3;3;4;5;4;3] *)

On termine le produit cartésien :

  1. let cartesian_product list1 list2 =
  2. let open Core.Std.List in
  3. concat @@
  4. list1 >>| fun elt1 ->
  5. list2 >>| fun elt2 ->
  6. (elt1, elt2)

L'emplacement du concat n'est pas très intuitif. En fait la fonction serait plus lisible s'il n'était pas du tout présent. C'est en fait possible, mais il faut changer la fonction map = (>>|) par une fonction combinant map et concat. C'est la fonction bind, et l'opérateur infixe (>>=), qui le font. Malheureusement bind n'est pas (encore) dans la librairie standard, mais on le définit facilement.

  1. let bind list funct =
  2. list
  3. |> List.map funct
  4. |> List.concat

  5. val Core.Std.List.bind : 'elt list -> ('elt -> 'im list) -> 'im list
  6. val Core.Std.List.(>>=) : 'elt list -> ('elt -> 'im list) -> 'im list

  7. let multiples_of n = [n; 2 * n; 3 * n]
  8. let res = bind [1;2;3] multiples_of
  9. (* res = [1;1;1;2;4;6;3;6;9] *)

  10. let cartesian_product list1 list2 =
  11. let open Core.Std.List in
  12. list1 >>= fun elt1 ->
  13. list2 >>= fun elt2 ->
  14. [ (elt1, elt2) ]

Notons que la fonction donnée en deuxième argument de bind doit retourner une liste, ce qui explique que la dernière ligne de cartesian_product soit une liste singleton, et non pas une simple paire.

bind est donc la fonction annoncée en début de section, qui prenant une liste, construit une nouvelle liste en remplaçant chaque élément $e$, par une liste d'éléments en fonction de $e$. Il suffit donc de lui donner la liste, et la fonction calculant la liste d'images de chaque élément.

La fonction bind est tellement importante que nous ferons une section complète sur comment l'utiliser, puis nous ferons un cours complet sur cette fonction dans d'autres contextes que celui des listes.

Récapitulatif.

  • filter et partition construisent des listes en ne gardant qu'une partie des éléments, selon un critère donnée comme une fonction booléenne. Ces fonctions contruisent des listes plus courtes.
  • map remplace chaque élément par un autre, individuellement. map contruit une liste de même taille que son argument.
  • append et concat concatènent deux ou plusieurs listes. bind construit une liste en remplaçant chaque élément par plusieurs (possiblement zéro).

Ces 6 fonctions ont donc des rôles bien déterminés, qui couvrent la plupart des transformations de liste à liste dont nous pouvons avoir besoin.

Retour au sommaire.