Contacter l'auteur Pierre.Weis@inria.fr

Fichier créé le 2 septembre 1998.
Version 1.1 du 14 septembre 2003.

Conseils de programmation Caml

Appel aux commentaires

Ceci est un ensemble de conseils raisonnables de présentation des programmes Caml, ainsi que des conseils de programmation qui ont recueilli l'assentiment de programmeurs Caml chevronnés. Cependant, tous les avis circonstanciés sur les éventuelles erreurs ou omissions seront pris en compte avec plaisir. Envoyez vos commentaires ici.

Merci à tous ceux qui ont déjà participé à la critique de cette page:

Daniel de Rauglaudre, Luc Maranget, Jacques Garrigue, Damien Doligez, Xavier Leroy, Bruno Verlyck, Bruno Petazzoni, Francois Maltey, Basile Starynkevitch, Toby Moth, Pierre Lescanne.

0 Conseils généraux pour l'écriture

Soyez simples et lisibles

Le temps passé à taper les programmes est négligeable par rapport au temps passé à les lire. C'est pourquoi on gagne un temps précieux à réaliser des programmes dont la lisibilité est optimale.

Tout le temps «perdu» aujourd'hui à rendre le programme plus simple vous sera rendu au centuple dans l'avenir lors des innombrables modifications et relectures (en commençant par la mise au point du programme).

Loi d'écriture des programmes: Un programme est écrit une fois, modifié 10 fois, relu 100 fois. Il faut donc simplifier l'écriture, penser aux modifications, toujours privilégier la facilité de lecture.

Méthode de résolution de problèmes

En Caml, une part non négligeable de la programmation consiste à définir les types de données adaptés au problème. Cette phase a une importance considérable, même si elle ne donne pas lieu à l'écriture de code exécutable. Elle met en effet en place la logique interne du programme et l'ensemble des invariants que le contrôleur de type de Caml vérifiera pour vous.

On peut donc conseiller une méthodologie en trois points:

Le quoi
La programmation commence par l'écriture des types qui décrivent le problème à résoudre.
Puis on décrit les «profiles» des différentes fonctions qui résoudront le problème; on donne leur type et l'on spécifie leurs données d'entrée et leurs résultats en sortie.
Le comment
Il s'agit maintenant d'écrire le code des fonctions dont les types ont été spécifiés précédemment et qui réaliseront effectivement la solution: on explicitera alors les méthodes ou algorithmes qui seront utilisés.
Le pas n'importe quoi
Pendant (voire même avant) la phase de rédaction des programmes, on aura soin d'écrire des fichiers d'exemples dont les données et les résultats sont connus avec certitude. Les programmes seront ensuite testés systématiquement sur ces exemples qu'on enrichira soigneusement de tous les cas d'erreur rencontrés lors de la mise au point.
Si ces tests ne prouveront bien entendu jamais que les programmes sont corrects, ils assureront au moins que les programmes ne font pas vraiment n'importe quoi!

Dans l'idéal, une quatrième phase consisterait à prouver formellement les programmes écrits; malheureusement cette étape est souvent hors d'atteinte, faute de temps, de connaissances et même parfois de capacité du programmeur.

Choix des types de données

Les types de données en Caml sont éminemment riches: on distingue 3 espèces de types, auxquels on peut indépendamment attribuer un genre de visibilité parmi trois 3, ce qui en modifie alors fortement l'usage et donc finalement la nature.

Les trois espèces de types

Les trois genres de types

Les trois genres servent à modéliser différents types de données et à assurer différentes propriétés:

Comparaison des genres

Construction des valeurs
Un type concret ne contrôle pas la construction de ses valeurs.
Un type abstrait contrôle fortement la construction: il doit nécessairement exporter des fonctions de constructions, mais n'est pas obligé d'offrir toute la panoplie de ses constructeurs (certains cas restant inaccessibles).
Tout comme un type abstrait, un type privé contrôle fortement la construction des valeurs et exporte des fonctions de constructions qui assurent les invariants qui s'impose.
Ajout de fonctionnalités
Un type concret est complètement ouvert: on peut utiliser le filtrage pour écrire toute nouvelle définition de fonction nécessaire.
Un type abstrait est en revanche complètement fermé: on ne peut ajouter de nouvelles fonctionnalités que par composition des fonctionnalités déjà offertes; dans la plupart des cas, il est difficile d'ajouter des fonctions imprévues car il est impossible de discriminer les valeurs par filtrage.
Tout comme un type concret, un type privé est également complètement ouvert à l'ajout de nouvelles fonctionnalités.
Sécurité de l'implémentation
Un type concret est complètement ouvert: il révèle parfaitement la façon dont il est construit et les utilisations extérieures du type concret en rendent la modification difficile.
Un type abstrait ne révèle rien de sa construction interne: on peut donc en changer complètement l'implémentation, sans que les utilisations du type n'en soient affectés.
Un type privé se situe à mi-chemin: il révèle une vue de ses valeurs, la façon dont les différents cas se présentent lorsque les invariants sont respectés. En revanche, les fonctions de constructions qui imposent les invariants peuvent être modifiées sans que les clients n'en soient affectés.

En conclusion: pour une structure libre, utilisez un type concret; pour une structure contrainte, utilisez un type privé; lorsque les différents cas du type doivent rester secrets ou lorsque l'ensemble des opérations possibles sur les valeurs doit être restreint, utilisez un type abstrait.

I Conseils de présentation des programmes

I Conventions lexicales

Pseudo loi des espaces: n'hésitez pas à séparer les mots de vos programmes à l'aide de blancs; la touche d'espacement est la plus facile à trouver, il n'y a pas de raison de s'en priver!
Délimiteurs
Les symboles délimiteurs sont suivis d'un espace, les symboles d'opérations sont entourés d'espaces. Ce fut un grand progrès de la typographie de séparer les mots par des blancs pour rendre les textes écrits plus faciles à lire. Faites de même dans vos programmes si vous voulez des textes lisibles.
Écriture des paires
Un n-uplet est parenthésé et la ou les virgules (délimiteurs) sont suivies d'un espace: (1, 2),
let triplet = (x, y, z) ....
Exceptions communément admises à cette règle:
Définition des composantes d'une paire
Au lieu de let (x, y) = ..., on peut écrire let x, y = ....
Justification:
il s'agit de la définition simultanée de plusieurs valeurs, non de la construction d'un n-uplet. En outre, le filtre est bien délimité entre let et =.
Filtrage simultané de plusieurs valeurs
On admet d'omettre les parenthèses des n-uplets dans le cas d'un filtrage simultané sur plusieurs valeurs.
      match x, y with
      | 1, _ -> ...
      | x, 1 -> ...
      | x, y -> ...
      
Justification:
il s'agit d'un filtrage en parallèle sur plusieurs valeurs, non de la construction d'un n-uplet. En outre, les expressions à filtrer sont délimitées par match et with, tandis que les filtres sont bien délimités par | et ->.
Écriture des listes
On écrit x :: l avec des espaces autour du symbole :: (car :: est un opérateur infixe donc entouré d'espaces) et [1; 2; 3] (car ; est un délimiteur donc suivi d'un espace).
Écriture des symboles d'opérations
On aura soin de bien séparer les symboles d'opération par des espaces: non seulement les formules sont plus lisibles, mais les confusions d'opérateurs multi-symboles sont évitées. (Exceptions évidentes à cette règle, les symboles «!» et «.» ne sont pas séparés de leurs arguments.)
Exemple: on écrit x + 1 ou x + !y.
Justification:
si l'on écrivait sans espaces, alors x+1 serait compris, mais x+!y changerait de signification puisque «+!» est analysé comme un opérateur infixe multi-symboles.
Critique:
l'absence d'espaces autour d'un opérateur améliore la lisibilité des formules quand on l'utilise pour refléter les priorités relatives des opérateurs. Par exemple
x*y + 2*z
met bien en évidence la priorité de la multiplication sur l'addition.
Réponse à la critique:
cette idée est séduisante mais elle est illusoire, parce que rien dans le langage ne donne l'assurance que les espaces reflètent bien la signification de la formule. Par exemple
x * z-1 signifie bien (x * z) - 1,
et non x * (z - 1) comme l'interprétation proposée des espaces semblerait le suggérer. D'autre part, le problème des opérateurs multi-symboles empêcherait d'utiliser cette convention de façon uniforme: on ne pourrait pas restreindre les espaces autour de la multiplication pour écrire x*!y + 2*!z. Enfin ce jeu sur les espaces est une convention ténue et subtile, un message sub-liminal difficile à saisir à la lecture: si vous voulez mettre en évidence les priorités, utilisez le moyen significatif que vous procure le langage: mettez des parenthèses.
Autre justification:
le cas des opérateurs infixes n'est plus un cas particulier compliqué; en effet, si l'on peut écrire (+) sans blancs, on ne peut pas écrire (*) puisque (* est évidemment compris comme le début d'un commentaire. Il faut donc écrire au minimum ( *), mais un blanc supplémentaire après * est vraiment préférable pour éviter aussi que *) ne soit compris, dans certains contextes, comme une fin de commentaire. Toutes ces difficultés sont évitées simplement en adoptant la règle simple de toujours mettre des blancs autour des opérateurs.
En fait, cette règle n'est pas si difficile à suivre: la barre d'espace est la plus grosse touche du clavier, c'est la plus facile à taper et vous ne pouvez pas la manquer!
Écriture des longues chaînes de caractères
On indentera les longues chaînes de caractères sur plusieurs lignes, en utilisant la convention de passage à la ligne (un caractère \ en fin de ligne) qui omet les blancs présents au début de la ligne suivante:
   let déclaration_universelle =
    "-1- Les logiciels naissent et demeurent libres et égaux en droit;\n\
     les distinctions ne peuvent être fondées que sur l'utilité commune." in
   ...
 

II Indentation des programmes

Pseudo loi de Landin: traitez l'indentation de vos programmes comme si elle déterminait la signification de vos programmes.

J'ajouterais à cette loi: traitez méticuleusement l'indentation de vos programmes car dans certains cas elle détermine vraiment leur signification!

L'indentation des programmes est un art qui suscite beaucoup de polémiques. On donne ici plusieurs styles d'indentation qui se sont dégagés de l'expérience et qui ne sont pas sévèrement critiqués.

Lorsqu'une justification du style adopté m'a paru évidente, je l'ai indiquée. À l'inverse, des critiques sont aussi notées.

À chaque fois, il vous faut donc choisir parmi les différents styles suggérés.
La seule règle impérative est:

Cohérence de l'indentation
Choisissez un style d'indentation généralement admis, puis utilisez-le systématiquement dans toute l'application.
Largeur de la page
La page dispose de 80 colonnes.
Justification:
cette largeur permet de lire les programmes sur tous les écrans et de les imprimer avec une fonte lisible sur une feuille standard.
Hauteur de la page
Une fonction devrait toujours tenir dans une page d'écran (d'environ 70 lignes), exceptionnellement deux, à l'extrême limite trois. Aller au-delà est déraisonnable.
Justification:
lorsqu'une fonction dépasse une page, il est temps de la découper en sous-problèmes traités indépendamment. Au-delà d'une page, on se perd dans le code, l'indentation est peu lisible et difficile à maintenir correcte.
Valeur de l'indentation
Les changements d'indentation des lignes de programme sont généralement de 1 ou 2 espaces. Adoptez une valeur d'indentation et respectez-la dans tout le programme.
Utilisation des taquets de tabulation
On décourage absolument l'utilisation du caractère de tabulation (caractère ASCII 9).
Justification:
d'un écran à l'autre, l'indentation du programme est complètement modifiée; elle est même complètement erronée si vous avez mélangé espaces et tabulations pour indenter votre programme.
Critique:
la présence de tabulation permet justement un réglage a posteriori de l'indentation par le lecteur du programme: s'il veut modifier globalement l'indentation, le lecteur n'aura qu'à modifier les taquets de tabulation et la structure de l'indentation du programme restera la même que dans le programme d'origine.
Réponse à la critique:
il me paraît presque impossible de mettre en oeuvre cette méthode car il faudrait n'utiliser que des tabulations pour indenter, sans jamais utiliser aucun espaces, ce qui me semble un exercice vraiment difficile.
Indentation des définitions globales let ... ;;
Le corps d'une fonction définie globalement dans un module est généralement indenté normalement. Il est cependant admis de traiter ce cas spécialement pour mieux mettre en valeur la définition.
D'autres conventions sont acceptables, par exemple:
Indentation des let ... in
L'expression qui suit une définition introduite par let est indentée au même niveau que le mot clé let, et le mot clé in qui l'introduit est écrit en fin de ligne:
  let expr1 = ... in
  expr1 + expr1
 
Dans le cas d'une série de let, la règle précédente implique que ces définitions soient placées au même niveau d'indentation:
  let expr1 = ... in
  let n = ... in
  ...
 
Justification:
on suggère qu'une série de «let ... in» est analogue à un ensemble d'hypothèses dans un texte mathématique, d'où l'indentation au même niveau de toutes les hypothèses.

Variante: certains écrivent le mot clé in seul sur une ligne, pour mettre en évidence l'expression finale du calcul:
  let e1 = ... in
  let e2 = ... in
  let new_expr =
   let e1' = derive_expression e1
   and e2' = derive_expression e2 in
   Add_expression e1' e2'
  in
  Mult_expression (new_expr, new_expr);;
 
Critique:
manque d'homogénéité.
Indentation des if then else
Alternatives multiples
  • Les conditions des alternatives multiples sont écrites au même niveau d'indentation:
           if cond1 ...
           if cond2 ...
           if cond3 ...
          
    Justification:
    traitement analogue aux clauses des filtrages, toutes alignées au même taquet de tabulation.

    Si la taille des conditions et des expressions le permet on écrira par exemple:
           if cond1 then e1 else
           if cond2 then e2 else
           if cond3 then e3 else
           e4
          

    Si les expressions dans les branches de conditionnelles multiples doivent être délimitées, on écrira:
           if cond then begin
             e1
            end else
           if cond2 then begin
             e2
            end else
           if cond3 then ...
          
  • Certains suggèrent une autre méthode d'écriture des conditionnelles multiples, où les lignes commencent par else:
           if cond1 ...
           else if cond2 ...
           else if cond3 ...
          
    Justification:
    elsif est un mot clé dans beaucoup de langages, on utilise donc l'indentation et else if pour le rappeler.
    Critique:
    manque d'homogénéité du traitement de toutes les conditions. Pourquoi un cas particulier pour la première condition ?
Encore une fois, choisissez votre style et utilisez-le systématiquement.
Alternatives simples
Plusieurs styles sont possibles pour les alternatives simples, selon la taille des expressions en jeu et surtout la présence de délimiteurs begin end pour ces expressions.
En cas de délimitation des branches de l'alternative, plusieurs styles sont préconisés:
  • style begin en fin de ligne
           if cond then begin
            e1
           end else begin
            e2
           end
           
  • ou encore premier begin en début de ligne:
           if cond then
            begin
             e1
            end else begin
             e2
            end
           
En fait l'indentation des conditionnelles dépend de la taille des expressions qui les composent.
  • Si cond, e1 et e2 sont petites on écrit simplement sur une ligne:
          if cond then e1 else e2
          
  • Si les expressions qui composent l'alternative sont purement fonctionnelles (ne font pas d'effets), on s'attachera à les lier par un let .. in lorsqu'elles sont trop grosses pour tenir sur la ligne.
    Justification:
    on retrouve alors l'indentation simple sur une ligne qui est la plus lisible. Accessoirement, le nommage aide à la relecture.
  • On considère donc maintenant le cas où les expressions considérées font des effets, ce qui empêche de les lier simplement par un let ... in.
    1. si e1 et cond sont petites, mais e2 grosse:
                  if cond then e1 else
                  e2
                  
    2. si e1 et cond sont grosses et e2 petite:
                  if cond then
                   e1
                  else e2
                  
    3. si toutes les expressions sont grosses:
                  if cond then
                   e1
                  else
                   e2
                  
    4. s'il y a des délimiteurs begin end
                  if cond then begin
                   e1
                  end else begin
                   e2
                  end
                  
    5. un mélange où e1 nécessite begin end mais e2 est petite
                  if cond then begin
                   e1
                  end else
                   e2
                  

      ou encore
                  if cond then begin
                   e1
                  end else e2
                  
Indentation des filtrages
Les principes généraux
  • Les clauses de filtrages sont toutes introduites par une barre verticale y compris la première.
Critique:
La première barre est facultative, inutile de l'écrire.
Réponse à la critique:
L'indentation ainsi obtenue paraît non naturelle : le premier cas est forcément plus indenté que le passage normal à la ligne ne l'exigerait. C'est donc une exception inutile à la règle d'indentation. C'est aussi une exception à la syntaxe utilisée pour toutes les autres clauses. Enfin, c'est esthétiquement douteux (certains disent même laid).
  • Les clauses de filtrages seront toutes alignées au niveau de la barre verticale qui commence chaque clause.
  • Si l'expression correspondant à une clause ne tient pas sur la ligne, on ira à la ligne immédiatement après la flèche de la clause (qui terminera donc la ligne), puis l'on indentera normalement, à partir du début du filtre correspondant.
  • Les flèches des clauses n'ont pas à être alignées.
match ou try
Pour un match ou un try les clauses seront alignées sur le début de la construction:
     match lam with
     | Abs (x, body) -> 1 + size_lambda body
     | App (lam1, lam2) -> size_lambda lam1 + size_lambda lam2
     | Var v -> 1
    
     try f x with
     | Not_found -> ...
     | Failure "not yet implemented" -> ...
    
Le mot clé with sera placé en fin de ligne. En cas de débordement de l'expression qui le précède, with apparaîtra seul sur une ligne:
     try
      let y = f x in
      if ...
     with
     | Not_found -> ...
     | Failure "not yet implemented" -> ...
    
Justification:
le mot clé with, seul sur une ligne, met bien en évidence l'entrée dans la partie filtrage de la construction.
Indentation des expressions des clauses
Si l'expression située à droite d'une flèche de filtrage ne tient pas sur la ligne, on ira à la ligne immédiatement après la flèche.
    match lam with
    | Abs (x, body) ->
       1 + size_lambda body
    | App (lam1, lam2) ->
       size_lambda lam1 + size_lambda lam2
    | Var v -> 1
   

Certains programmeurs prétendent que cette règle doit être appliquée à toutes les clauses, dès que l'une des expressions déborde. Ils indenteraient donc la dernière clause du filtrage précédent ainsi:
    | Var v ->
       1
   

D'autres vont encore plus loin en appliquant cette règle systématiquement à toutes les clauses filtrages.
   let rec fib = function
     | 0 ->
        1
     | 1 ->
        1
     | n ->
        fib (n - 1) + fib ( n - 2);;
   
Critique:
manque de compacité du code; pour des filtrages simples (ou des clauses simples dans des filtrages complexes), cette règle n'apporte rien à la lisibilité.
Justification:
Je ne vois pas de justification raisonnable, sauf si vous êtes payé à la ligne de code: adopter cette règle est alors un bon moyen d'écrire plus de lignes de code Caml, sans risquer d'introduire des bugs supplémentaires.
Filtrages de fonctions anonymes
Comme pour match ou try, les filtrages des fonctions anonymes, introduits par function, sont alignés sur le mot-clé function:
    map
     (function
      | Abs (x, body) -> 1 + size_lambda 0 body
      | App (lam1, lam2) -> size_lambda (size_lambda 0 lam1) lam2
      | Var v -> 1)
     lambda_list
    
Filtrages de fonctions nommées
Les filtrages des fonctions définies par let ou let rec donnent lieu à plusieurs styles raisonnables qui respectent les règles générales d'indentations des clauses de filtrage (sauf la règle concernant les fonctions anonymes évidemment). Les styles d'indentation préconisés sont décrits dans le paragraphe concernant les les définitions globales. Choisissez le style qui vous convient le mieux, et utilisez toujours le même!
   let rec size_lambda accu = function
     | Abs (x, body) -> size_lambda (succ accu) body
     | App (lam1, lam2) -> size_lambda (size_lambda accu lam1) lam2
     | Var v -> succ accu
   
   let rec size_lambda accu = function
   | Abs (x, body) -> size_lambda (succ accu) body
   | App (lam1, lam2) -> size_lambda (size_lambda accu lam1) lam2
   | Var v -> succ accu
   
Mauvaise indentation des filtrages
Pas d'indentation sauvage  des fonctions et des analyses de cas.
Cela consiste à indenter directement sous le mot-clé match ou function qui a préalablement été ferré à droite. N'écrivez pas:
    let rec f x = function
                  | [] -> ...
                   ...
    

mais indentez plutôt la ligne au mot-clé let:
    let rec f x = function
      | [] -> ...
                   ...
    
Justification:
avec l'indentation sauvage, on se cogne dans la marge et l'esthétique est douteuse ...
Pas non plus d'alignement sauvage  des «->» des clauses de filtrage.
On considère comme de mauvaise pratique de soigneusement placer toutes les flèches d'un filtrage au même taquet de tabulation, comme dans le code suivant:
    let f = function
      | C1          -> 1
      | Long_name _ -> 2
      | _           -> 3;;
    
Justification:
l'indentation sauvage des flèches se traduit par une exception désagréable à l'indentation normale; cela rend plus difficile la maintenance du programme (l'ajout d'un cas supplémentaire peut amener à modifier l'indentation de toutes les lignes ou bien ... l'abandon pur et simple de l'alignement des flèches; mieux vaut donc commencer directement par là et ne pas les aligner du tout!).
Indentation des appels de fonctions
Indentation calée au nom de la fonction:
Le problème ne se pose que pour les fonctions ayant beaucoup d'arguments ou bien des arguments très complexes qui ne peuvent tenir sur la même ligne. On s'efforcera d'indenter les expressions par rapport au nom de la fonction (1 ou 2 espaces selon la convention adoptée). Les petits arguments seront écrits sur la même ligne et l'on changera de ligne au début d'un argument.
Dans toute la mesure du possible on évitera les arguments constitués d'une expression complexe: dans ce cas on définira le «gros» argument par une construction let.
Justification:
le problème d'indentation disparaît; en outre, si le nom donné aux expressions est significatif, le code est bien plus lisible.
Justification supplémentaire:
si l'évaluation des arguments produit des effets, la liaison let est de toute façon nécessaire pour expliciter l'ordre d'évaluation.
Nommage des arguments complexes:
Au lieu de
     let tampon =
      f x y z
        «grosse
        expression»
        «autre grosse»
        expression» in
     ...
     

on écrira
     let t =
       «grosse
       expression»
     and u =
       «autre grosse»
       expression» in
     let tampon =
      f x y z t u in
     ...
     
Nommage des fonctions immédiates:
En cas d'itérateur dont l'argument est une fonction complexe, on définit aussi la fonction par une liaison let.
Au lieu de
    List.map
     (function x ->
       blabla
       blabla
       blabla)
     l
    

on écrira
    let f x =
     blabla
     blabla
     blabla in
    List.map f l
    
Justification:
c'est bien plus clair, surtout si le nom donné à la fonction est significatif.
Indentation des opérations
Lorsqu'une opération comporte des arguments complexes, ou en présence de multiples appels à la même opération, l'on terminera la ligne à l'opérateur, et l'on n'indentera pas le reste des opérations. Par exemple (ici la barre verticale | de fin de ligne représente la marge droite de la ligne):
 x + y + z + |
 t + u       |
 
Justification:
terminer la ligne sur l'opération indique bien que l'expression n'est pas terminée.
En cas de «grosse expression» dans une telle suite d'opération, on préfèrera encore une fois définir la «grosse expression» à l'aide d'une construction let in plutôt que d'avoir à l'indenter en ligne.
Au lieu de
  x + y + z +      |
  «grosse          |
   expression»     |
  

on écrira:
  let t =
   «grosse         |
    expression» in |
  x + y + z + t    |
  
La liaison des expressions trop grosses pour être écrites dans une opération s'impose franchement en cas de combinaison d'opérateurs.
Au lieu de l'expression peu lisible
  (x + y + z * t) /   |
  («grosse            |
    expression»)      |
  

on écrira
  let u =
   «grosse            |
    expression» in    |
  (x + y + z * t) / u |
  
Ces conseils s'étendent à tous les opérateurs. Par exemple:
  let u =
   «grosse         |
    expression» in |
  x :: y ::        |
  z + 1 :: t :: u  |
  

II Conseils de programmation

I Méthode de programmation

Toujours sur le métier remettez votre ouvrage,
Et puis le polissez et le repolissez.

II Commentaires dans les programmes

N'hésitez pas à commenter lorsqu'il y a une difficulté.
S'il n'y a pas de difficulté, il n'y a pas lieu de commenter.
Évitez les commentaires dans le corps des fonctions.
Préférez un commentaire au début de la fonction, qui explique le fonctionnement de l'algorithme utilisé. Encore une fois, s'il n'y a pas de difficulté, il n'y a pas lieu de commenter.
Évitez les commentaires nocifs.
Un commentaire nocif est un commentaire qui n'apporte aucune valeur ajoutée, c'est-à-dire aucune information autre que triviale. Le commentaire nocif n'a évidemment pas d'intérêt; il est même dangereux car il distrait inutilement le lecteur. On l'utilise souvent pour remplir des critères dits de métrologie du logiciel, par exemple le rapport (nombre de lignes de code) / (nombre de lignes de programme) qui mesure parfaitement un rapport dont j'ignore l'intérêt. Proscrire absolument les commentaires nocifs.
Voici un bel exemple, car les mots employés sont techniques et le commentaire passe ainsi facilement pour un vrai commentaire quand en fait son contenu est vide:
 (*
  Fonction print_lambda:
  imprime une lambda-expression passée en argument.

  Arguments: lam, une lambda expression quelconque.
  Résultats: aucun.

  Remarque: print_lambda ne peut être utilisée que pour son effet.
 *)
 let rec print_lambda lam =
  match lam with
  | Var s -> printf "%s" s
  | Abs l -> printf "\\ %a" print_lambda l
  | App (l1, l2) ->
     printf "(%a %a)" print_lambda l1 print_lambda l2;;
 
Mode d'emploi dans l'interface du modules
Le mode d'emploi de la fonction doit apparaître dans l'interface du module qui l'exporte, pas forcément dans le programme qui l'implémente. On choisira des commentaires à la manière des interfaces de modules du système Caml, ce qui permettra ensuite d'extraire automatiquement la documentation de l'interface du module si besoin est.
Utilisez les assertions:
Les assertions permettent d'éviter les commentaires verbeux, tout en permettant une vérification utile à l'exécution.
Par exemple, les conditions de validité des arguments de fonction sont utilement vérifiées par des assertions.
 let f x =
  assert (x >= 0);
  ...
 

Notez en outre qu'une assertion est souvent préférable à un commentaire car elle est plus fiable: une assertion est pertinente car elle est vérifiée à chaque exécution (même si le programme est compilé ordinairement avec l'option du compilateur qui omet les assertions, il est simple de remettre l'option de vérification en cas de comportement étrange du programme); au contraire, un commentaire n'a aucun contenu sémantique et peut donc facilement devenir obsolète: au lieu d'être une aide, il se transforme alors souvent en un obstacle à la bonne compréhension du programme.
Commentaires ligne à ligne du code impératif:
Bien sûr, lorsqu'on écrit du code très difficile, et particulièrement en cas de code avec beaucoup d'effets mémoire (modifications physiques de structures de données), il est quelquefois indispensable de commenter dans le corps des fonctions, pour expliquer en quoi le programme implémente réellement l'algorithme décrit dans l'entête de la fonction, ou encore pour suivre l'évolution des invariants que la fonction doit maintenir. Encore une fois, lorsqu'il y a des difficultés il faut absolument commenter, à chaque ligne si nécessaire.

III Choix des identificateurs

Il est difficile de choisir des identificateurs dont le nom évoque la signification du morceau de programme correspondant. C'est pourquoi on y attachera un soin particulier, en privilégiant la clarté et la régularité du nommage.

Exemple:

let add_expression expr1 expr2 = ...
let print_expression expr = ...

On tolèrera une exception à la recommandation de ne pas utiliser les changements de casse pour séparer les mots des identificateurs dans le cas d'interfaçage avec des bibliothèques existantes qui utilisent cette convention de nommage: cela permet aux utilisateurs Caml de la bibliothèque de se repérer plus aisément dans la documentation originale de la bibliothèque.

IV Utilisation des parenthèses dans les expressions

Les parenthèses sont significatives: elles indiquent la nécessité d'utiliser une priorité inhabituelle. Elles doivent donc être utilisées à bon escient et non pas saupoudrées au hasard dans les programmes. Pour cela, vous devez connaître les priorités habituelles, c'est-à-dire les combinaisons d'opérations qui ne nécessitent pas de parenthèses. Fort heureusement ce n'est pas compliqué pour qui connaît un peu de mathématiques ou s'efforce de suivre les règles suivantes:

V Délimitation des constructions dans les programmes

Lorsqu'il est nécessaire de délimiter des constructions syntaxiques dans les programmes, on utilise comme délimiteurs les mots-clés begin et end plutôt que des parenthèses. Cependant, l'utilisation de parenthèses est acceptable si vous le faites de façon cohérente, c'est-à-dire systématique.

Cette délimitation explicite des constructions concerne essentiellement deux constructions: les filtrages imbriqués et les séquences imbriquées dans les constructions if then else.

match dans un match
Lorsqu'une construction match ... with ou try ... with apparaît dans une clause de filtrage, il faut impérativement délimiter cette construction imbriquée (sinon les clauses du filtrage englobant sont automatiquement associées au filtrage englobé). Par exemple:
match x with
| 1 ->
  begin match y with
  | ...
  end
| 2 ->
...
Séquences dans les branches d'un if
De même une séquence qui apparaît dans la partie then ou else d'une alternative doit être délimitée:
if cond then begin
 e1;
 e2
end else begin
 e3;
 e4
end

VI Utilisation des modules

Découpage en modules

Il faut découper les programmes en modules cohérents.

Pour chaque module, il faut écrire une interface explicite.

Pour chaque interface, il faut écrire la documentation des entités définies par le module, fonctions, types, exceptions, etc.

Ouverture des modules

Éviter les directives open. Préférer la notation qualifiée des identificateurs. On privilégie donc des noms de modules courts mais significatifs.

Justification:
l'utilisation d'identificateurs non qualifiés est ambiguë et donne lieu à des erreurs sémantiques difficiles à détecter.
 let lim = String.length name - 1 in
 ...

 let lim = Array.length v - 1 in
 ...

 ... List.map succ ...

 ... Array.map succ ...

Modules utilisés ouverts, modules utilisés fermés

On considère qu'il est normal d'ouvrir un module qui modifie l'environnement, et apporte d'autres versions d'un ensemble important de fonctions. Par exemple, le module Format fournit l'impression indentée automatiquement. Ce module redéfinit les fonctions d'impression habituelles print_string, print_int, print_float, etc. Quand on utilise Format, on l'ouvre donc systématiquement en début de fichier.
Si on ne le faisait pas, on courerait le risque d'oublier la qualification d'une fonction d'impression, ce qui passerait la plupart du temps inaperçu car des fonctions ayant le même nom que celles de Format existent dans l'environnement par défaut (Pervasives). Ce mélange produirait des erreurs dans l'affichage très difficile à retrouver dans le code source. Considérons par exemple

 let f () =
  Format.print_string "Hello World!"; print_newline ();;
cette fonction est subtilement erronée, car elle n'appelle pas Format.print_newline pour vider la queue du pretty-printer, si bien que "Hello World!" reste en attente dans Format, tandis que Pervasives.print_newline écrit un simple retour charriot sur la sortie standard ... Si Format imprime sur un fichier tandis que la sortie standard est reliée au terminal les choses se corsent pour trouver qu'un retour charriot manque dans le fichier (et que l'impression sur le fichier est bizarre puisque des boîtes qui devaient être fermées par Format.print_newline sont restées ouvertes), tandis qu'un innocent retour-charriot est apparu sur le terminal!

Pour la même raison, on ouvre aussi les grosses bibliothèques comme celle des grands entiers afin d'alléger le programme qui les utilise.

open Num;;

let rec fib n =
 if n <= 2 then Int 1 else fib (n - 1) +/ fib (n - 2);;
Justification:
le programme serait moins lisible si l'on devait qualifier tous les identificateurs.

Dans un programme où les définitions de type sont partagées, il est bon de réunir ces définitions dans un ou des module(s) sans implémentation (ne contenant que des types). Il est alors admis d'ouvrir systématiquement le module qui exporte les définitions de type partagées.

VII Filtrages

On n'aura jamais peur d'abuser du filtrage!

En revanche, on aura soin d'éviter les filtrages non exhaustifs: ils seront complétés avec soin, sans utiliser une clause ``attrappe-tout'' comme | _ -> ... ou | x -> ... quand il est possible de s'en passer (par exemple lorsqu'il s'agit de filtrages sur des types concrets définis dans le programme). Voir aussi les les avertissements du compilateur.

VIII Avertissements du compilateur

Les avertissements du compilateurs sont destinés à prévenir des erreurs potentielles; c'est pourquoi il faut impérativement en tenir compte et corriger vos programmes si leur compilation produit de tels avertissements. De plus, les programmes dont la compilation produit des avertissements ont un parfum d'amateurisme qui ne sied certainement pas à vos propres oeuvres!

Avertissements sur le filtrage
Les avertissements concernant le filtrage doivent être traités avec le plus grand soin:
  • Ceux qui concernent les clauses inutiles doivent bien entendu être éliminés.
  • Pour les filtrages non-exhaustifs on complètera le filtrage correspondant, sans ajouter un cas par défaut «attrape-tout», comme | _ -> ... , mais avec la liste explicite des constructeurs non examinés dans le reste du filtrage, par exemple | Cn _ | Cn1 _ -> ... .
    Justification:
    cette écriture n'est pas vraiment plus complexe, et permet l'évolution plus sûre du programme. En effet l'ajout d'un nouveau constructeur au type de données filtré produira à nouveau une alerte, ce qui permettra au programmeur d'ajouter la clause correspondante au nouveau constructeur s'il y a lieu. Au contraire, la clause «attrape-tout» rendra la compilation de la fonction silencieuse et l'on pourra penser que la fonction est correcte alors que le nouveau constructeur sera traité par le cas par défaut.
  • Les filtrages non-exhaustifs induits par des clauses avec gardes doivent aussi être corrigés. Un cas typique consiste à supprimer une garde redondante. Ce problème est décrit en détail dans la FAQ du langage.
let destructurant
La construction let ne sert pas seulement à définir des identificateurs: on peut l'utiliser avec des filtres plus complexes ou plus simples qu'un seul identificateur. Par exemple
  • let avec filtres complexes:
    let [x; y] as l = ...
    permet de définir simultanément une liste l et les deux éléments qu'elle contient x et y.
  • let avec filtre très simple:
    let _ = ... ne définit rien du tout, se contentant en fait d'évaluer l'expression à droite du signe =.
Cependant, si le filtre utilisé par let introduit un filtrage partiel, on n'a aucun moyen de corriger ce filtrage dangereux (c'est le cas par exemple pour let [x; y] as l = List.map succ (l1 @ l2) qui échouera si le résultat de map n'a pas exactement deux éléments (ou si le résultat de map n'a pas au moins deux éléments si l'on avait écrit let (x :: y :: _ as l) = List.map succ (l1 @ l2)).
Le let destructurant ne doit pas produire d'avertissement
On n'utilisera donc le let destructurant que dans le cas où le filtrage est exhaustif. Dans ce cas, on dit que le filtre est irréfutable. Typiquement, on se limitera donc à des définitions de type produits (n-uplets ou enregistrements) ou à des définitions dont le filtre appartient à un type somme à un seul cas. Dans tous les autres cas, on préfèrera une construction match ... with explicite.
  • let ... in: les let destructurant qui donnent un avertissement du compilateur seront systématiquement remplacés par un filtrage explicite. Par exemple, au lieu de let [x; y] as l = List.map succ (l1 @ l2) in expression, on écrira:
        match List.map succ (l1 @ l2) with
        | [x; y] as l -> expression
        | _ -> assert false
        
  • Pour les définitions globales à l'aide de let destructurants, on réécrira la définition avec un filtrage explicite et en utilisant des n-uplets intermédiaires:
        let x, y, l =
         match List.map succ (l1 @ l2) with
         | [x; y] as l -> x, y, l
         | _ -> assert false
        
Justification:
il n'y a pas de moyen de rendre le filtrage exhaustif si l'on utilise des let destructurant généraux.
Avertissements sur la séquence et let _ = ...
Lorsque le compilateur émet un avertissement sur le type d'une expression d'une séquence, on doit explicitement indiquer qu'on désire ignorer le résultat de cette expression. Pour cela,
  • on utilise une liaison vide, et pour supprimer l'avertissement de:
       List.map f l;
       print_newline ()
      

    on écrit
       let _ = List.map f l in
       print_newline ()
      
  • on utilise aussi la fonction prédéfinie ignore : 'a -> unit qui ignore son argument pour rendre unit. On écrit alors
       ignore (List.map f l);
       print_newline ()
      
  • mais le meilleur moyen est encore de réfléchir sur son code et de se demander pourquoi l'on obtient cet avertissement. Le compilateur émet une alerte parce que le programme calcule un résultat inutile puisqu'il est jeté. Donc, s'il n'est complètement inutile, ce calcul ne sert que par ses effets; il devrait donc le signaler en retournant la valeur rien. La plupart du temps l'alerte indique qu'on n'a pas appelé la bonne fonction, ou bien que la fonction nécessaire n'existe pas et doit être écrite. Ici, on est dans le premier cas, et il fallait évidemment écrire
       List.iter f l;
       print_newline ()
      

    Dans les programmes, il arrive que la version procédurale (celle qui ne fait que les effets de bord) de la fonction n'existe pas et qu'il faille l'écrire: très souvent une séparation des parties fontionnelle et purement procédurale de la fonction résoud élégamment le problème et le programme gagne en lisibilité! Par exemple, on pourrait réécrire la définition suivante (qui mélange allègrement calculs et effets de bord)
      let add x y =
        if x > 1 then print_int x;
        print_newline ();
        x + y;;
      
    en deux définitions séparées:
      let print_adder x =
        if x > 1 then print_int x;
        print_newline ();;
    
      let add x y = x + y;;
      
    et modifier en conséquence les appels à add.
Attention cependant, la construction let _ = ... ne doit être utilisée que ponctuellement, et pour expliciter un résultat ignoré. Elle ne doit évidemment pas remplacer systématiquement la séquence.
Justification:
la séquence est bien plus claire!
Comparer e1; e2; e3 à
       let _ = e1 in
       let _ = e2 in
       e3
 

IX Fonctions hd et tl

On n'utilisera pas les fonctions hd et tl mais un filtrage explicite de la liste argument.

Justification:
c'est tout aussi bref et bien plus clair que l'usage de hd et tl qui doit nécessairement être protégé par un try... with... pour rattraper l'exception qui peut être déclenchée par ces fonctions.

X Boucles

Boucles for

Pour parcourir simplement un vecteur ou une chaîne on utilisera une boucle for.

 for i = 0 to Array.length v - 1 do
  ...
 done

Si la boucle est complexe ou rend un résultat on utilisera une fonction récursive.

let find_index e v =
 let rec loop i =
  if i >= Array.length v then raise Not_found else
  if v.(i) = e then i else loop (i + 1) in
 loop 0;;
Justification:
la fonction récursive permet de coder simplement n'importe quelle boucle, même complexe, par exemple à sorties multiples ou avec des pas d'indices bizarres (un pas dépendant de la valeur d'une donnée par exemple).
En outre, la boucle récursive évite d'utiliser des mutables dont la valeur peut-être modifiée dans n'importe quelle partie du corps de la boucle (ou même à l'extérieur): au contraire la boucle récursive prend explicitement en argument les variables dont les valeurs sont susceptibles de changer lors des appels récursifs.

Boucles while

Loi des boucles while: Attention: une boucle while est habituellement fausse, à moins que son invariant de boucle n'ait été explicité.

La principale utilisation de la boucle while est la boucle sans fin while true do .... On en sort par une exception, généralement à la terminaison du programme.

Les autres boucles while sont très difficiles d'emploi, à moins qu'elles ne proviennent de programmes tout faits des cours d'algorithmique où elles ont été prouvées.

Justification:
les boucles while nécessitent un ou des mutables pour que la condition de la boucle change de valeur et que la boucle finalement termine. Pour prouver la correction, il faut donc mettre à jour des invariants de boucle, sport intéressant mais difficile.

XI Exceptions

Ne pas craindre de définir ses propres exceptions dans les programmes, mais à l'inverse utiliser au maximum les exceptions prédéfinies du système. Par exemple toute fonction de recherche qui échoue devrait déclencher l'exception prédéfinie Not_found. Ayez soin de surveiller les exceptions qui peuvent être déclenchées par les appels de fonction à l'aide d'un try ... with.

L'utilisation d'un rattrapage de toutes les exceptions par un try ... with _ -> est normalement réservée à la fonction principale du programme. Si l'on a besoin de rattraper toutes les exceptions pour préserver un invariant d'un algorithme, on aura soin de nommer l'exception et de la relancer, après avoir remis en place l'invariant. Typiquement:

let ic = open_in ...
and oc = open_out ... in
try
 treatment ic oc;
 close_in ic; close_out oc
with x -> close_in ic; close_out oc; raise x
Justification:
try ... with _ -> rattrape silencieusement toutes les exceptions même celles qui n'ont rien à voir avec le calcul en cours (par exemple une interruption sera capturée et le calcul continuera malgré tout!).

XII Structures de données

L'une des grandes forces de Caml réside dans la puissance des structures de données définissables et la simplicité de leur manipulation. Il faut donc en profiter au maximum et ne pas hésiter à définir ses propres structures de données. En particulier, il ne faut pas représenter systématiquement des énumérations par des entiers, ni des énumérations à deux cas par des booléens. Exemples:

type figures =
   | Triangle | Carré | Cercle | Parallélogramme;;
type convexité =
   | Convexe | Concave | Quelconque;;
type type_de_définition =
   | Récursive | Non_récursive;;
Justification:
une valeur booléenne masque souvent l'intuition du codage correspondant. Par exemple, si type_de_définition est codé par un booléen, que signifie true ? Définition «normale» (c'est-à-dire non récursive) ou définition récursive ?
dans le cas d'un type énuméré codé par un entier, il est très difficile de limiter l'intervalle des entiers acceptables: il faut définir des fonctions de construction qui assurent les invariants du programme (et s'assurer ensuite qu'on ne construit jamais de valeurs autrement qu'en appelant ces fonctions de construction), ou bien truffer le programme d'assertions ou encore de gardes dans les filtrages. Ces problèmes sont élégamment résolus par la définition d'un type somme énuméré, qui permet ensuite d'utiliser à plein le filtrage en profitant du test d'exhaustivité du filtrage que vous offre le compilateur.
Critique:
dans le cas des énumérations binaires, on peut systématiquement définir des prédicats dont le nom porte la sémantique des booléens qui implémentent le type. Supposons aussi que l'on adopte la convention d'indiquer qu'un nom représente un prédicat lorsque ce nom se termine par la lettre p (comme en emacs lisp). Alors, au lieu de définir un type somme pour type_de_définition, on définirait simplement un prédicat recursivep qui rend vrai si la définition est récursive.
Réponse à la critique:
cette méthode ne peut être étendue aux énumérations autres que binaires; de plus, elle se marie moins naturellement avec le filtrage. Prenons le cas des définitions récursives ou non, dont la nature serait codée directement par un booléen, provenant donc du cas | Let of bool * string * expression d'un type définition; un cas de filtrage typique sur une définition aura l'allure suivante:
| Let (_, v, e) as def ->
   if recursivep def then ``code pour le cas récursif''
   else ``code pour le cas non récursif''
on encore, si le prédicat recursivep s'applique directement aux booléens:
| Let (b, v, e) ->
   if recursivep b then ``code pour le cas récursif''
   else ``code pour le cas non récursif''
En revanche, avec un type explicite on écrirait plutôt:
| Let (Récursive, v, e) -> ``code pour le cas récursif''
| Let (Non_récursive, v, e) -> ``code pour le cas non récursif''
la différence est faible il est vrai, et l'on peut préférer l'un ou l'autre style selon ses goûts. Remarquez cependant que la méthode avec type binaire explicitement défini n'empêche nullement de définir un prédicat pour se ramener au cas utilisant directement des booléens. Note finale: dans le cas de l'utilisation directe des booléens, la définition d'un prédicat n'empêche pas un autre programmeur d'utiliser directement les booléens (en écrivant par exemple | Let (b, v, e) -> if b then ... ce qui est bien typé et tombe dans le travers de sémantique peu claire dénoncé plus haut, ce que le langage interdit lorsqu'on définit un type somme énuméré).

A contrario, il ne faut pas systématiquement définir des drapeaux booléens à l'aide d'un nouveau type, lorsque l'interprétation des constructeurs true et false paraît claire. On peut ainsi discuter de l'utilité des types suivants:

type switch = | On | Off;;
type bit = | One | Zero;;

La même objection est recevable pour les types énumérés représentés par des entiers, lorsque les entiers ont une interprétation évidente pour les données à représenter.

XIII Utilisation des mutables

Les valeurs mutables sont utiles et quelquefois indispensables à une programmation simple et claire. Il faut cependant les utiliser avec discernement: les structures de données normales de Caml sont non mutables. On privilégie les structures de données non mutables pour la clarté et la sûreté de programmation qu'elles autorisent.

XIV Itérateurs

Les itérateurs de Caml sont un trait puissant et utile. Il ne faut cependant pas en abuser, ni a contrario les négliger: il vous sont fournis par les bibliothèques et ont toutes les chances d'être corrects et mûrement réfléchis par l'auteur de la bibliothèque. Il est donc inutile de les réinventer.

On écrira donc

let carre_elements elements = map carre elements;;

plutôt que:

let rec carre_elements = function
  | [] -> []
  | elem :: elements -> carre elem :: carre_elements elements;;

En revanche, on évitera d'écrire:

let iterator f x l =
 List.fold_right (List.fold_left f) [List.map x l] l;;

Quoiqu'on obtienne:

val iterator : ('a list -> 'a -> 'a list) -> ('a -> 'a) -> 'a list -> 'a list =
  <fun>
# iterator (fun l x -> x :: l) (fun l -> List.rev l) [[1; 2; 3]] ;;
- : int list list = [[1; 2; 3]; [3; 2; 1]]

En cas de besoin exprès, on aura soin d'ajouter un commentaire explicatif: à mon avis il s'impose!

XV Optimisation des programmes

Pseudo loi de l'optimisation:

Pas d'optimisation a priori.

Pas d'optimisation a posteriori non plus.

Programmer simple et clair avant tout. Ne commencer l'optimisation que lorsque le goulet d'étranglement du programme a été identifié (en général quelques routines). Alors l'optimisation consiste avant tout à changer la complexité de l'algorithme utilisé. Cela passe souvent par la redéfinition des structures de données manipulées et la réécriture complète de la partie du programme qui pose problème.

Justification:
clarté et correction des programmes sont prioritaires. De plus, dans un programme conséquent, il est pratiquement impossible d'identifier a priori les parties du programme dont l'efficacité est primordiale.

XVI Choisir entre classes et modules

Les classes d'Objective Caml s'imposent lorsqu'on a besoin d'héritage, c'est-à-dire d'un raffinement incrémental des données et de leurs fonctionnalités.

Les types de données conventionnels (types somme en particulier) s'imposent lorsqu'on a besoin de filtrage.

Les modules s'imposent lorsque les structures de données sont fixes et que leur fonctionnalité est également fixée ou que l'ajout de nouvelles fonctions dans les programmes qui les utilisent est suffisant.

III Clarté du Code Caml

Le langage Caml comporte des constructions puissantes qui autorisent une programmation simple et claire. Le principal problème pour obtenir un programme limpide est de les utiliser à bon escient...

Le langage propose de nombreux styles de programmation (ou paradigmes de programmation): la programmation impérative, la programmation fonctionnelle, la programmation orientée objet. Le premier travail du programmeur est donc d'employer le paradigme de programmation adapté au problème. Au sein d'un paradigme de programmation, la difficulté est d'employer la construction du langage qui exprime le plus simplement et le plus naturellement le calcul à effectuer.

Écueils dans le style
En ce qui concerne les styles de programmation, on observe principalement les deux écueils symétriques du «tout impératif» (on utilise systématiquement des boucles et des affectations) et du «tout fonctionnel» (on n'utilise jamais de boucles ni d'affectation); le style «tout objet» apparaîtra bientôt, n'en doutons pas, mais il est encore trop nouveau pour être décrit ici. Par exemple:
  • Écueil du «Trop impératif»:
    • On aurait tort d'utiliser un style impératif pour coder une fonction naturellement récursive. Par exemple, pour calculer la longueur d'une liste, on ne devrait pas écrire:
        let list_length l =
         let l = ref l in
         let res = ref 0 in
         while !l <> [] do
          incr res; l := List.tl !l
         done;
         !res;;
        
      au lieu de la fonction récursive toute simple:
        let rec list_length = function
          | [] -> 0
          | _ :: l -> 1 + list_length l;;
        
      (pour ceux qui contesteraient l'équivalence des deux programmes, voir la note suivante).
    • Un autre écueil du trop impératif est souvent de ne pas utiliser systématiquement la simple boucle for pour parcourir un vecteur, mais au contraire d'utiliser une boucle while complexe, avec une ou des références (trop d'affectations inutiles et trop d'occasions d'erreur).
    • Ce programmeur considère que le mot-clé mutable dans les définitions de types enregistrement devrait être implicite.
  • Écueil du «Trop fonctionnel»:
    • Le programmeur frappé de ce syndrome évite d'utiliser les vecteurs et l'affectation. Dans les cas les plus graves on assiste à un refus complet d'utiliser les aspects impératifs du langage, même quand ils sont de loin les plus naturels.
    • Des symptômes caractéristiques: réécriture systématique des boucles for à l'aide d'une fonction récursive, utilisation de listes dans des contextes où des structures de données impératives s'imposent, passage de nombreux paramètres globaux du problème à toutes les fonctions, même quand une référence globale permettrait d'omettre ces paramètres pour la plupart invariants.
    • Ce programmeur considère que le mot-clé mutable dans les définitions de types enregistrement devrait être supprimé de Caml.
Code Caml habituellement considéré comme illisible
La puissance des constructions disponibles en Caml permet d'écrire du code limpide et extrêmement concis. Cependant cette puissance des constructions autorise aussi l'écriture de code inutilement complexe, jusqu'à obtenir un programme parfaitement illisible.
Voici un certains nombre d'écueils connus:
  • Classique, l'introduction de conditionnelles if then else absolument inutiles, comme
        let flush_ps () =
          if not !psused then psused := true;;
        
    ou (plus subtilement)
        let sync b =
          if !last_is_dvi <> b then begin last_is_dvi := b end;;
        
  • Plus déroutant, le codage d'une construction par une autre. Par exemple, coder un let ... in par l'application d'une fonction anonyme à un argument. On écrirait
        (fun x y -> x + y)
         e1 e2
        
    au lieu d'écrire simplement
        let x = e1
        and y = e2 in
        x + y
        
  • Dans le même esprit, le codage systématique de la séquence avec des let in est assez ``populaire''.
  • Utiliser des conditionnelles inutiles (donc nocives); par exemple
        let flush_ps () =
          if not !psused then psused := true;;
        
    ou (plus subtile encore)
        let sync b =
          if !last_is_dvi <> b then begin last_is_dvi := b end;;
        
  • Encore plus méchant, le mélange des calculs et des effets, particulièrement dans les appels de fonctions. Rappelons que l'ordre dans lequel sont évalués les arguments d'une application n'est pas spécifié, ce qui implique qu'on ne doit pas mélanger effets et calculs dans les appels de fonctions. Cependant, lorsqu'il n'y a qu'un argument à la fonction, on peut en profiter pour faire un effet dans l'argument, ce qui est extrêmement troublant pour le lecteur bien que sans danger pour la sémantique du programme. À proscrire absolument.
  • Plus spécifique aux langages de haut niveau, l'utilisation abusive (c'est à dire excessive, ou mal à propos, ou encore notoirement insuffisante) des itérateurs et des fonctions d'ordre supérieur. Par exemple, il vaut mieux utiliser List.map ou List.iter que d'écrire en ligne leurs équivalents en terme de fonction récursive. Pire encore, ne pas utiliser List.map ou List.iter mais leur équivalent en ligne, exprimé avec List.fold_right ou List.fold_left.
  • La reine des méthodes pour obtenir un code illisible est évidemment de mélanger toutes ou partie des méthodes précédentes. Par exemple:
    (fun u -> print_string "world"; print_string u)
     (let temp = print_string "Hello"; "!" in
      ((fun x -> print_string x; flush stdout) " ";
       temp));;
    
Si vous écrivez naturellement ainsi le programme print_string "Hello world!", vous pouvez sans doute soumettre vos oeuvres au concours du code Caml le plus illisible.

IV Gestion du développement des programmes

On donne ici les recettes des programmeurs Caml confirmés, celles qui ont servi à développer les compilateurs qui sont de bons exemples de gros programmes complexes, développés en petite équipe.

Édition des programmes

Beaucoup de développeurs nourrissent une espèce de vénération pour l'éditeur Emacs (gnu-emacs en général) qu'ils utilisent pour écrire leurs programmes. L'éditeur est bien interfacé avec le langage puisqu'il est capable de colorer les sources Caml (mise en couleur des différentes catégories de mots, coloration des mots-clés par exemple), de recompiler les sources avec deux ou trois appuis de touches, et de positionner le curseur directement sur le fichier et à l'endroit où s'est produite une erreur de compilation.

Les deux commandes suivantes sont donc considérées comme indispensables:

Les développeurs décrivent ainsi l'utilisation de ces fonctionnalités: la combinaison CTRL-C-CTRL-C recompile toute l'application; en cas d'erreurs, une succession de CTRL-X-` permet la correction de toutes les erreurs signalées; le cycle recommence avec une nouvelle recompilation lancée par CTRL-C-CTRL-C.

Autres astuces d'Emacs

La commande ESC-/ (dynamic-abbrev-expand) complète automatiquement le mot placé devant le curseur avec l'un des mots présents dans l'un des fichiers en cours d'édition. Cela permet donc de toujours choisir des identificateurs significatifs sans avoir l'ennui de devoir taper des noms à rallonge dans ses programmes: la commande ESC-/ complètera facilement l'identificateur après la frappe des premières lettres. En cas de complétion inadéquate, chaque ESC-/ supplémentaire propose une autre complétion.

Sous Unix, la combinaison d'une commande, suivie de next-error (CTRL-X-`) est aussi utilisée pour chercher toutes les occurrences d'une certaine chaîne de caractères dans un programme Caml. Au lieu de lancer make pour recompiler, on lance la commande grep -n avec pour arguments la chaîne à chercher et les noms des fichiers où la chercher, par exemple Meta-X grep -n type_expression *.ml trouvera toutes les occurrences de l'identificateur type_expression dans tous les fichiers sources du répertoire courant; ensuite les ``messages d'erreur'' de grep -n sont compatibles avec l'utilisation de CTRL-X-` qui positionnera automatiquement le curseur sur le fichier et la prochaine occurrence de la chaîne trouvée.

Édition avec le système interactif

Sous Unix: on utilise l'éditeur ligne ledit qui offre de grandes capacités d'éditions ``à la emacs'' (y compris ESC-/ !), ainsi qu'un mécanisme d'historique qui permet de retrouver les commandes précédemment tapées et même de retrouver les commandes d'une session à l'autre. ledit est écrit en Caml et est librement disponible ici.

Compilation

L'utilitaire make est indispensable pour gérer la compilation et la recompilation des programmes. Des modèles de fichiers make sont disponibles pour Caml Light et Objective Caml. On peut aussi consulter les Makefiles des compilateurs Caml.

Gestion de versions et développements simultanés

Les utilisateurs du système cvs de gestion de versions des logiciels ne tarissent pas d'éloges sur les gains de productivité qu'il leur apporte. Ce système permet de gérer les développements d'une équipe de programmeurs en leur imposant une certaine cohérence et la gestion d'un journal des modifications apportées au logiciel.
cvs autorise aussi les développements simultanés de plusieurs équipes, éventuellement dispersées sur plusieurs sites reliés au réseau. Les compilateurs Caml sont tous «sous» cvs.

Un serveur CVS anonyme (camlcvs.inria.fr) contenant les sources de travail des compilateurs Caml et d'autres logiciels relatifs à Caml a été rendu disponible à tous. Vous pouvez ainsi obtenir une copie locale des compilateurs dont la mise à jour est particulièrement aisée (il suffit de taper cvs update).

Notes

Versions impérative et fonctionnelle de list_length

Les deux versions de list_length ne sont pas parfaitement équivalentes en terme de complexité, puisque la version impérative s'exécute en espace de pile constant, alors que la version fonctionnelle nécessite la sauvegarde dans la pile d'exécution des adresses de retour des appels récursifs en suspend (dont le nombre maximal est exactement égal à la longueur de la liste argument). Si l'on veut écrire un programme s'exécutant en espace de pile constant, point n'est besoin d'utiliser un style impératif, il suffit d'écrire une fonction récursive en queue (tail-rec) en utilisant un accumulateur:

  let list_length l =
   let rec loop accu = function
   | [] -> accu
   | _ :: l -> loop (accu + 1) l in
   loop 0 l;;
  
On obtient ainsi un programme ayant les mêmes caractéristiques que le programme impératif, sans pour autant perdre le côté naturel de l'algorithme opérant par filtrage sur la structure de données argument.


Page de présentation de Caml Dernière modification: vendredi 2 avril 2004
Copyright © 1995 - 2004, INRIA tous droits réservés.

Contacter l'auteur Pierre.Weis@inria.fr