Cours 5

Htmligure
Version & licenses
Creative Commons License

Les combinateurs

Guyslain
Thursday, 21 July 2016

Retour au sommaire.

Les combinateurs.

Un des traits principaux de la programmation fonctionnelle est la capacité à manipuler des fonctions de la même façon que toutes les autres données. Cela permet par exemple de pouvoir passer des fonctions en arguments, ou bien de pouvoir en retourner.

De façon plus générale, il faut même comprendre que les fonctions sont elle-mêmes des données : on peut donc les utiliser pour représenter des entités, dès lors que c'est une représentation adaptée. On peut donc représenter nos données par des tuples, des enregistrements, des types algébriques, des objets ou bien par des fonctions.

Ce cours va principalement montrer à quel point cette idée de représenter par des fonctions peut être naturelle et conduire et des programmes très simples. Pour cela, nous allons voir quelques exemples.

Tout cela est donc permis par le fait que les fonctions peuvent être passées en arguments d'autres fonctions, ou retournées par d'autres fonctions. Une fonction travaillant sur des fonctions s'appelle une fonctionnelle, ou une fonction d'ordre supérieure. Nous avons déjà introduit plein de fonctionnelles. Par exemple, sur les listes, les fonctions exists, map, filter, fold_left, etc. sont des fonctionnelles puisqu'elles prennent toutes une fonction en argument.

Mais nous avons aussi vu que la plupart de ces fonctions peuvent être vues, non comme des fonctions travaillant sur des listes, mais comme des fonctions transformant une fonction normale en fonction sur les listes. Elles prennent en argument une fonction, et retourne une fonction sur les listes.

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

Par exemple, ce parenthésage du type de map met en évidence que map construit une fonction à partir d'une fonction. Les fonctions transformant des fonctions en fonctions sont en général appelées des combinateurs.

Examinons un exemple plus complet que nous avons déjà croisé plus tôt. Lorsque nous avons parlé des consommateurs, nous avons introduit un type pour les prédicats, ainsi qu'un certain nombre d'opérations :

  1. module Predicate :
  2. sig
  3. type 'value t
  4. val test : 'value t -> 'value -> bool
  5. val always : 'value t (* always true *)
  6. val never : 'value t (* always false *)
  7. val negate : 'value t -> 'value t
  8. val is : 'value -> 'value t
  9. val checks : ('value -> bool) -> 'value t
  10. val or_else : 'value t -> 'value t -> 'value t
  11. val and_also : 'value t -> 'value t -> 'value t
  12. val all : 'value t -> 'value list t
  13. val exists : 'value t -> 'value list t
  14. val both : 'left t -> 'right t -> ('left * 'right) t
  15. val either : 'left t -> 'right t -> ('left * 'right) t
  16. val comap : ('value -> 'testable) -> 'testable t -> 'value t

  17. val (||) : 'value t -> 'value t -> 'value t (* or_else *)
  18. val (&&) : 'value t -> 'value t -> 'value t (* and_also *)
  19. val (=|<) : ('value -> 'testable) -> 'testable t -> 'value t (* comap *)
  20. end

Il est temps de voir comment programmer ce module. La première question à se poser est : qu'est-ce qu'un prédicat ? C'est une fonction vers l'ensemble des booléens. À partir de cette réponse, le type à utiliser pour représenter les prédicats est évident :

  1. type 'value t = 'value -> bool

On utilise donc un type fonction. Immédiatement, cela implique que presque toutes les fonctions du module Predicate sont en fait des combinateurs ! Voyons un peu. Commençons par les définitions les plus simples :

  1. let test = (@@)
  2. let always : 'any t = fun value -> true
  3. let never : 'any t = fun value -> false

test consiste simplement à appliquer le prédicat à la valeur à tester. L'application se note (@@). always et never sont des prédicats : des fonctions prenant une valeur et retournant un booléen, ici toujours le même.

  1. let negate predicate = fun value -> not (predicate value)
  2. let is this = fun value -> this = value
  3. let checks property = property

negate inverse le prédicat, c'est donc une fonction qui prend une valeur, et retourne la négation de ce que dit le prédicat. is pourrait s'écrire plus astucieusement let is = (=). Comme quoi nous sommes en train de manipuler des fonctions ! De même, checks doit transformer une fonction de type 'value -> bool en un prédicat, de même type : c'est donc l'identité. On passe aux fonctions intéressantes :

  1. let or_else predicate1 predicate2 =
  2. fun value -> predicate1 value || predicate2 value
  3. let and_also predicate1 predicate2 =
  4. fun value -> predicate1 value && predicate2 value

On prend deux fonctions en arguments, on en retourne une nouvelle.

  1. let all = List.for_all
  2. let exists = List.exists

Et oui, List.for_all et List.exists sont des combinateurs : elles transforment comme promis des prédicats en prédicats.

  1. let both left_predicate right_predicate =
  2. fun (left,right) -> left_predicate left && right_predicate right
  3. let either left_predicate right_predicate =
  4. fun (left,right) -> left_predicate left || right_predicate right
  5. let comap fct predicate =
  6. fun value -> predicate (fct value)

  7. let (||) = or_else
  8. let (&&) = and_also
  9. let (=|>) = comap

Pas de difficulté particulière pour ces trois dernières fonctions. Globalement, écrire ce module n'a pas couté bien cher, toutes les fonctions s'écrivent en une ligne, et plusieurs sont même complètement triviales. Pourtant, il peut se révéler utile. Imaginons un type pour coder les identités de personnes, avec des accesseurs pour obtenir diverses informations (nom, âge, etc.). On peut maintenant trouver toutes les personnes d'une liste vérifiant certains critères :

  1. let criterion =
  2. let open Predicate in
  3. get_age =|> ((<=) 10 && (>) 35))
  4. && get_genre =|> is Female
  5. && get_siblings =|> exists (get_genre =|> is Male)

  6. let ok_people =
  7. List.filter criterion people_database

Le même code sans le module Predicate :

  1. let criterion person =
  2. let is_brother sibling = get_genre sibling = Male in
  3. (10 <= get_age person && get_age person < 35))
  4. && (get_genre person = Female)
  5. && List.exists is_brother (get_siblings persons)

  6. let ok_people =
  7. List.filter criterion people_database

est quand même bien plus verbeux !

Retour au sommaire.