Author: Christian.Queinnec@lip6.fr

## Quel style choisir ?

Il n'est pas ici question de religion ni d'esthétique, il n'y a pas a priori un style plus joli ou meilleur que l'autre. En revanche, selon le problème à résoudre un style peut être plus adéquat que l'autre.
La première règle à appliquer est celle de la simplicité. L'algorithme à réaliser (qu'il soit écrit dans un livre ou qu'il soit à l'état de germe dans l'esprit du programmeur) est lui-même décrit dans un certain style; il est naturel d'utiliser le même style pour en faire l'implantation.
Le second critère de choix est l'efficacité du programme. On peut dire qu'un programme impératif (bien écrit) est plus efficace que son homologue fonctionnel, mais dans de très nombreux cas la différence n'est pas suffisamment notable pour justifier de compliquer le code en adoptant un style impératif là où le fonctionnel serait naturel. La fonction `map` de la section précédente fournit un bel exemple de problème naturellement exprimé dans le style fonctionnel et qui ne gagne rien à être écrit en impératif.

### Séquence ou composition de fonctions

Nous avons vu que dès qu'un programme effectue des effets de bord, il est nécessaire d'expliciter précisément l'ordre d'évaluation des éléments de ce programme. Ceci peut se faire dans les deux styles :

fonctionnel :
  en utilisant le fait que Objective CAML est un langage strict, c'est-à-dire que l'argument est calculé avant l'application de la fonction. L'expression `(`f`` `(`g`` x`)``)` est calculée en évaluant d'abord `(`g`` x`)`, puis en passant le résultat comme argument de `f`. Avec des expressions plus complexes, on peut nommer le résultat intermédiaire avec la construction let in mais le principe reste le même : let`` aux`=``(`g`` x`)``` in`` `(`f`` aux`)`.
impératif :
  en utilisant la séquence ou les autres structures de contrôle (boucles). Dans ce cas, le résultat n'est pas une valeur rendue par une fonction, mais ses effets de bord sur la mémoire : aux`:=``(`g`` x`)``` ;`` `(`f`` `!`aux`)`.

Examinons ce problème de choix de style sur un exemple. L'algorithme de tri rapide sur un vecteur se décrit récursivement comme ceci :

1. choisir un pivot : choisir l'indice d'un élément du tableau;
2. permuter autour du pivot : permuter les éléments du tableau de sorte que les éléments inférieurs au pivot soient situés avant le pivot et vice et versa;
3. trier (suivant le même algorithme) les deux tableaux obtenus : les éléments précédant le pivot et ceux le suivant.

Le choix de l'algorithme, à savoir de modifier un tableau pour que ses éléments soient triés, nous incite à utiliser un style impératif au moins pour les manipulations des données.
On commence par définir une fonction permutant deux éléments d'un tableau.

    # let permute_element tab n p =
       let aux = tab.(n) in tab.(n) <- tab.(p) ; tab.(p) <- aux  ;;
    val permute_element : 'a array -> int -> int -> unit = <fun>

Le choix d'un bon pivot est déterminant pour l'efficacité de l'algorithme, mais nous retenons ici le choix le plus simple : rendre l'indice du premier élément du (sous) tableau.

    # let choisir_pivot tab debut fin = debut ;;
    val choisir_pivot : 'a -> 'b -> 'c -> 'b = <fun>

Écrivons l'algorithme que nous souhaitons utiliser pour permuter les éléments du tableau, autour du pivot.

1. on positionne le pivot au début du tableau en le permutant;
2. i est l'indice de second élément du tableau;
3. j est l'indice du dernier élément du tableau;
4. si l'élément d'indice j est plus grand que le pivot on le permute avec l'élément d'indice i et on incrémente i; sinon on décrémente j;
5. tant que i est inférieur à j on recommence l'opération précédente;
6. rendu à ce point chaque élément d'indice inférieur à i (ou j) est inférieur au pivot, les autres lui sont supérieurs; si l'élément d'indice i est inférieur au pivot on le permute avec le pivot, sinon on permute son prédécesseur.

L'implantation de cet algorithme va naturellement adopter des structures de contrôle impératives.

    # let permute_pivot tab debut fin ind_pivot =
        permute_element tab debut ind_pivot ;
        let i = ref (debut+1) and j = ref fin and pivot = tab.(debut) in
        while !i < !j do
          if tab.(!j) >= pivot then decr j
          else
            begin
              permute_element tab !i !j ;
              incr i
            end
        done ;
        if tab.(!i) > pivot then decr i ;
        permute_element tab debut !i ;
        !i
     ;;
    val permute_pivot : 'a array -> int -> int -> int -> int = <fun>

Outre son effet sur le tableau, cette fonction retourne en résultat l'indice du pivot.
Il ne nous reste plus qu'à composer les différentes étapes et mettre en place la récursion sur les sous-tableaux.

    # let rec quick tab debut fin =
        if debut < fin
        then
          let pivot = choisir_pivot tab debut fin in
          let place_pivot = permute_pivot tab debut fin pivot in
          quick (quick tab debut (place_pivot-1)) (place_pivot+1) fin
        else tab ;;
    val quick : 'a array -> int -> int -> 'a array = <fun>

Nous avons ici utilisé les deux styles. Le pivot choisi sert d'argument à la permutation autour de ce pivot, et le rang du pivot obtenu après permutation est un argument de l'appel récursif. Par contre, le tableau obtenu après permutation n'est pas retourné par la fonction `permute_pivot`, ce résultat est rendu par effet de bord. En revanche, la fonction `quick` retourne un tableau et le tri des sous-tableaux est obtenu par composition des appels récursifs.
La fonction principale est :

    # let quicksort tab = quick tab 0 ((Array.length tab)-1) ;;
    val quicksort : 'a array -> 'a array = <fun>

C'est une fonction polymorphe car la relation d'ordre `<` sur les éléments du tableau est elle même polymorphe.

    # let t1 = [|4;8;1;12;7;3;1;9|] ;;
    val t1 : int array = [|4; 8; 1; 12; 7; 3; 1; 9|]
    # quicksort t1 ;;
    - : int array = [|1; 1; 3; 4; 7; 8; 9; 12|]
    # t1 ;;
    - : int array = [|1; 1; 3; 4; 7; 8; 9; 12|]
    # let t2 = [|"le"; "petit"; "chat"; "est"; "mort"|] ;;
    val t2 : string array = [|"le"; "petit"; "chat"; "est"; "mort"|]
    # quicksort t2 ;;
    - : string array = [|"chat"; "est"; "le"; "mort"; "petit"|]
    # t2 ;;
    - : string array = [|"chat"; "est"; "le"; "mort"; "petit"|]

### Partage ou copie de valeurs

Tant que les valeurs que nous manipulons ne sont pas modifiables, il n'est pas utile de savoir si elles sont partagées ou non.

    # let id x = x ;;
    val id : 'a -> 'a = <fun>
    # let a = [ 1; 2; 3 ] ;;
    val a : int list = [1; 2; 3]
    # let b = id a ;;
    val b : int list = [1; 2; 3]

Que `b` soit une copie de la liste `a` ou que se soit la même liste ne change rien puisque ce sont de toutes façons des valeurs intangibles. Mais si à la place d'entiers nous mettons des valeurs modifiables, nous avons besoin de savoir si la modification d'une valeur entraîne la modification de l'autre.
L'implantation du polymorphisme d'Objective CAML effectue une copie des valeurs immédiates et le partage des valeurs structurées. Bien que le passage des arguments soit toujours par valeur, seul le pointeur d'une valeur structurée est copié. C'est bien le cas de la fonction `id` :

    # let a = [| 1 ; 2 ; 3 |] ;;
    val a : int array = [|1; 2; 3|]
    # let b = id a ;;
    val b : int array = [|1; 2; 3|]
    # a.(1) <- 4 ;;
    - : unit = ()
    # a ;;
    - : int array = [|1; 4; 3|]
    # b ;;
    - : int array = [|1; 4; 3|]

Nous avons ici un vrai choix de programmation pour décider quelle est la manière la plus efficace de représenter une donnée. D'un côté, l'utilisation de valeurs modifiables permet de faire des manipulations en place (c'est-à-dire sans faire d'allocation) mais oblige, dans certains cas, à faire des copies là où l'utilisation d'une valeur non modifiable aurait permis de faire un partage. Nous illustrons ceci avec deux façons d'implanter les listes.

    # type 'a liste_fixe = LFnil | LFcons of 'a * 'a liste_fixe ;;
    # type 'a liste_modifiable = LMnil | LMcons of 'a * 'a liste_modifiable ref ;;

Les listes fixes sont strictement équivalentes aux listes d'Objective CAML alors que les listes modifiables sont plus dans un style à la C où un maillon est une valeur plus une référence sur le maillon suivant.
Avec les listes fixes, il n'y a qu'une seule façon d'écrire la concaténation et elle impose de dupliquer la structure de la première liste; en revanche, la seconde liste peut être partagée avec le résultat.

    # let rec concat l1 l2 = match l1 with
         LFnil -> l2
       | LFcons (a,l11) -> LFcons(a, (concat l11 l2)) ;;
    val concat : 'a liste_fixe -> 'a liste_fixe -> 'a liste_fixe = <fun>

    # let lf1 = LFcons(1, LFcons(2, LFnil))
     and lf2 =  LFcons(3, LFcons(4, LFnil)) ;;
    val lf1 : int liste_fixe = LFcons (1, LFcons (2, LFnil))
    val lf2 : int liste_fixe = LFcons (3, LFcons (4, LFnil))
    # let lf3 = concat lf1 lf2 ;;
    val lf3 : int liste_fixe =
      LFcons (1, LFcons (2, LFcons (3, LFcons (4, LFnil))))
    # lf1==lf3 ;;
    - : bool = false
    # let tlLF l =  match l with
       LFnil       -> failwith "Liste vide"
     | LFcons(_,x) -> x ;;
    val tlLF : 'a liste_fixe -> 'a liste_fixe = <fun>
    # tlLF(tlLF(lf3)) == lf2 ;;
    - : bool = true

Grâce à ces exemples, nous voyons que les premiers maillons de `lf1` et de `lf3` sont distincts alors que la seconde partie de `lf3` est exactement `lf2`.
Avec les listes modifiables, nous avons le choix entre modifier les arguments (fonction `concat_share`) ou créer une nouvelle valeur (fonction `concat_copy`)

    # let rec concat_copy l1 l2 =  match l1 with
         LMnil  -> l2
       | LMcons (x,l11) -> LMcons(x, ref (concat_copy !l11 l2)) ;;
    val concat_copy :
      'a liste_modifiable -> 'a liste_modifiable -> 'a liste_modifiable = <fun>

Cette première solution, `concat_copy`, donne un résultat similaire à la fonction précédente `concat`. Voici une seconde solution qui partage entièrement arguments et résultat.

    # let concat_share l1 l2 =
      match l1 with
        LMnil -> l2
      | _     -> let rec set_last = function
                    LMnil       -> failwith "concat_share : cas impossible !!"
                  | LMcons(_,l) -> if !l=LMnil then l:=l2 else set_last !l
                 in
                   set_last l1 ;
                   l1 ;;
    val concat_share :
      'a liste_modifiable -> 'a liste_modifiable -> 'a liste_modifiable = <fun>

La concaténation avec partage ne demande aucune allocation (on n'utilise pas le constructeur `LMcons`), on se contente de faire pointer le dernier maillon de la première liste vers la seconde. Mais alors, la concaténation est susceptible de modifier les arguments qui sont passés à la fonction.

    # let lm1 = LMcons(1, ref (LMcons(2, ref LMnil)))
     and lm2 = LMcons(3, ref (LMcons(4, ref LMnil))) ;;
    val lm1 : int liste_modifiable =
      LMcons (1, {contents=LMcons (2, {contents=LMnil})})
    val lm2 : int liste_modifiable =
      LMcons (3, {contents=LMcons (4, {contents=LMnil})})
    # let lm3 = concat_share lm1 lm2 ;;
    val lm3 : int liste_modifiable =
      LMcons (1, {contents=LMcons (2, {contents=LMcons (...)})})

On obtient bien le résultat escompté pour `lm3`. Par contre la valeur liée à `lm1` été modifiée.

    # lm1 ;;
    - : int liste_modifiable =
    LMcons (1, {contents=LMcons (2, {contents=LMcons (...)})})

Cela peut donc avoir des conséquences dans le reste du programme.

### Éléments de choix

Dans un programme purement fonctionnel, il est interdit d'effectuer des effets de bord, cela prohibe la manipulation de structures de données mutables, les exceptions et les entrées-sorties. Nous donnons une définition moins restrictive du style fonctionnel en disant qu'une fonction ne modifiant pas son environnement global peut être utilisée dans un style fonctionnel. Une telle fonction peut manipuler localement des valeurs mutables (et donc être écrite dans un style impératif) mais ne doit pas modifier de variables globales ni ses arguments. Nous les autorisons de surcroît à lever des exceptions. Vu de l'extérieur, ces fonctions peuvent être considérées comme des << boites noires >> dont le comportement est assimilable à celui d'une fonction purement fonctionnelle si ce n'est qu'elles sont susceptibles de rompre le contrôle par le déclenchement d'une exception. Dans le même esprit, une valeur mutable qui n'est plus modifiée après son initialisation peut être utilisée dans un style fonctionnel.
D'un autre coté, un programme écrit dans un style impératif bénéficie tout de même des avantages apportés par Objective CAML : la sûreté du typage statique, la gestion automatique de la mémoire, le mécanisme d'exception, le polymorphisme paramétrique et l'inférence de types.
Le choix entre style impératif et style fonctionnel dépend de l'application à développer. On peut néanmoins indiquer certaines orientations selon les caractéristiques de celle-ci et les critères jugés important dans son développement.

- choix des structures de données : de l'utilisation de structures de données mutables ou non va découler le choix du style de programmation à adopter. En effet, le style fonctionnel est par nature incompatible avec la modification des valeurs mutables. Par contre, la construction et le parcours d'une valeur sont les mêmes quelque soit son statut. Nous rejoignons la discussion sur << modification en place vs copie >> tenue à la page ??. Nous y reviendrons en évoquant les critères d'efficacité.
- structures de données imposées : si le programme doit modifier des données mutables, le style impératif est le seul possible. Si par contre, il est juste nécessaire de parcourir les valeurs, alors l'adoption du style fonctionnel garantit l'intégrité des données.
  L'utilisation de structures de données récursives entraînent l'usage de fonctions elles-mêmes récursives. Celles-ci peuvent être définies en utilisant les traits de l'un ou de l'autre des styles, mais il est souvent plus facile de résonner par création d'une valeur suivant une définition par récurrence ce qui correspond à une approche fonctionnelle, que d'itérer des traitements par récursion sur cette valeur. Le style fonctionnel permet de définir des itérateurs génériques sur la structure de données manipulée, ce qui factorise le développement et le rend plus rapide.
- critères d'efficacité : la modification en place est incomparablement plus efficace que la création d'une valeur. Quand l'efficacité du code est un critère prépondérant, il fera le plus souvent penché la balance vers le style impératif. Notons cependant que la nécessité de départager les valeurs peut s'avérer une tâche très ardue et en définitive plus coûteuse que de copier les valeurs dès le départ.
  La pleine fonctionnalité a un coût : l'application partielle et l'utilisation de fonctions passées en argument d'autres fonctions ont un coût à l'exécution plus important que celui de l'application totale d'une fonction provenant d'une déclaration. L'usage de ce trait éminemment fonctionnel doit donc être proscrit des parties d'un programme où l'efficacité est critique.
- critères de développement : le plus haut niveau d'abstraction de la programmation fonctionnelle permet d'écrire plus rapidement, un code plus compact et contenant moins d'erreurs que son équivalent impératif qui par nature est plus verbeux. Le style fonctionnel apparaît comme avantagé pour les contraintes que posent le développement d'applications conséquentes. L'indépendance d'une fonction par rapport à son contexte d'évaluation rend le code fonctionnel plus facilement découpable en petites unités examinables séparément et en fin de compte plus lisible.
  La plus grande modularité du style fonctionnel avec la possibilité de passer des fonctions (donc des traitements) en argument d'autres fonctions fait que les programmes écrits dans ce style sont plus aisément réutilisables.

Ces quelques remarques montrent qu'il est souvent judicieux de mélanger les deux styles de programmation dans une application. Le style fonctionnel est plus rapide à développer et confère une organisation plus simple de l'application; mais les sections critiques en temps d'exécution auront intérêt à être développé dans un style impératif plus efficace.