English version
Accueil     À propos     Téléchargement     Ressources     Contactez-nous    

Ce site est rarement mis à jour. Pour les informations les plus récentes, rendez-vous sur le nouveau site OCaml à l'adresse ocaml.org.

Conseils de programmation Caml

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. Pour envoyez vos commentaires:

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.

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.

Conseils de présentation des programmes

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.

Ecriture 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) ....

É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
   ...

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 la première ci-dessous.

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: 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.

Indentation habituelle de 1 ou 2 espaces:

let f x = function
    | C ->
    | D ->
    ...;;

  let g x =
   let tmp =
    match x with
    | C ->
    | x -> 0 in
   tmp + 1;;

Justification: Pas d'exception dans la valeur de l'indentation.

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

  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.

si e1 et cond sont petites, mais e2 grosse:

  if cond then e1 else
    e2

si e1 et cond sont grosses et e2 petite:

  if cond then
    e1
  else e2

Si toutes les expressions sont grosses:

  if cond then
    e1
  else
    e2

S'il y a des délimiteurs begin end

  if cond then begin
    e1
  end else begin
    e2
  end

un mélange où e1 nécessite begin end mais e2 est petite

  if cond then begin
    e1
  end else e2

Indentation des filtrages

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 dans les 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 dans les 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). Voir plus haut pour les styles recommandés.
   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 to the function's name: 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 temp =
      f x y z
       ``large
       expression''
       ``other large''
       expression'' in
     ...
on écrira
     let t =
       ``large
       expression''
     and u =
       ``other large''
       expression'' in
     let temp =
      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) /
  (``large
    expression'')
on écrira
  let u =
   ``large
    expression'' in
  (x + y + z * t) / u
Ces conseils s'étendent à tous les opérateurs. Par exemple:
  let u =
   ``large
    expression'' in
  x :: y ::
  z + 1 :: t :: u

Conseils de programmation

Méthode de programmation

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

Programmez simple et clair

Quand c'est fini, relisez, simplifiez et clarifiez. À toutes les étapes de la création, utilisez votre tête!

Découpez vos programmes en petites fonctions

Les petites fonctions sont plus faciles à maîtriser.

Mettez en facteur les morceaux de code répétés en les définissant dans des fonctions séparées

Le partage du code ainsi obtenu facilite la maintenance puisque toute correction ou amélioration est automatiquement répercutée. En outre, le simple fait d'isoler et de nommer un morceau de code permet quelquefois d'identifier une fonctionnalité insoupçonnée.

N'utilisez pas le copier-coller en programmant

Coller du code signifie à coup sûr l'introduction d'un défaut de partage du code du programme et une négligence à identifier une fonction auxiliaire utile; donc cela signifie qu'on perd du partage dans le code du programme. La perte de partage de code conduit à des difficultés dans la maintenance: une erreur dans le code copié doit être corrigé à chaque occurrence du bug dans chacune des copies!

De surcroît, il est difficile de reconnaître les mêmes 10 lignes de code repétées 20 fois dans un programme. En revanche, si ces 10 lignes sont définies par une fonction auxiliaire, il n'y a aucun problème à savoir où ces lignes sont utilisées: ce sont tous les sites d'appel de la fonction auxiliaire. Couper-coller du code rend donc les programmes plus difficiles à comprendre.

En conclusion, copier-coller du code produit des programmes plus difficiles à lire, à comprendre et à maintenir: il faut absolument l'éviter.

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 module.

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.

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.

Ne pas utiliser d'abréviations pour les noms globaux

Les identificateurs globaux (y compris et surtout ceux des fonctions) peuvent être longs, car il importe de comprendre à quoi ils servent loin de leur définition.

Séparer les mots par des soulignés (int_of_string, pas intOfString)

Les changements de casse ont une forte portée sémantique dans les programmes Caml: les mots commençant par des majuscules sont réservés aux constructeurs et aux noms de modules en OCaml. De surcroît, les noms de valeurs globales doivent obligatoirement commencer par une minuscule. Ces règles empêchent d'employer la convention de façon cohérente: le premier mot doit forcément commencer par une minuscule et il est interdit d'appeler une fonction IntOfString.

Toujours nommer du même nom les arguments de fonctions qui ont la même signification

Si nécessaire, on explicitera ce nommage dans un commentaire en tête de fichier); s'il y a plusieurs arguments de même signification on les suffixera par des numéros.

Les identificateurs locaux peuvent être brefs, et doivent être repris d'une fonction à l'autre

Cela augmente la régularité du style. Éviter d'utiliser des identificateurs dont l'aspect peut prêter à confusion comme l ou O, faciles à confondre avec 1 et 0.

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.

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:

Opérateurs arithmétiques: mêmes règles qu'en mathématique

Par exemple: 1 + 2 * x means 1 + (2 * x).

Applications de fonctions: mêmes règles qu'en mathématique dans l'usage des fonctions trigonométriques

En mathématique, on écrit sin x pour signifier sin (x). De même sin x + cos x signifie (sin x) + (cos x) pas sin (x + (cos x)). On utilise les même conventions en Caml: on écrit f x + g x pour signifier (f x) + (g x).
Cette convention est généralisée à tous les opérateurs (infixes): f x :: g x signifie (f x) :: (g x), f s ^ g s' signifie (f s) ^ (g s'), failwith s ^ s' signifie (failwith s) ^ s' et pas failwith (s ^ s').

Comparaisons et opérateurs booléens

Les comparaisons sont des opérateurs infixes, donc les règles précédentes s'appliquent. C'est pourquoi f x < g x signifie (f x) < (g x). Pour des raisons de typage (pas d'autre interprétation sensée), l'expression f x < x + 2 signifie (f x) < (x + 2). De même f x < x + 2 && x > 3 signifie ((f x) < (x + 2)) && (x > 3).

Les priorités relatives des opérateurs booléens sont celles des mathématiques

Bien que les mathématiciens aient tendance à abuser des parenthèses dans ce cas, l'opérateur ``ou'' des booléens est analogue à l'addition et le ``et'' à la multiplication. Donc, de même que 1 + 2 * x signifie 1 + (2 * x), true || false && x signifie true || (false && x).

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

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.

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.

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:

let déstructurant

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

Le let déstructurant 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.

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,

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

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.

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.

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!).

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 figure =
   | Triangle | Square | Circle | Parallelogram;;
type convexity =
   | Convex | Concave | Other;;
type type_of_definition =
   | Recursive | Non_recursive;;

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: 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 for recursive case''
   else ``code for non recursive case''

on encore, si le prédicat recursivep s'applique directement aux booléens:

| Let (b, v, e) ->
   if recursivep def then``code for recursive case''
   else ``code for non recursive case''

En revanche, avec un type explicite on écrirait plutôt:

| Let (Recursive, v, e) -> ``code for recursive case''
| Let (Non_recursive, v, e) -> ``code for non recursive case''

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.

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.

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 square_elements = function
  | [] -> []
  | elem :: elements -> square elem :: square_elements elements;;

En revanche, on évitera d'écrire:

let iter 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!

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.

Choisir entre classes et modules

Les classes d'OCaml 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.

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:

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:

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.

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 sur La Bosse. 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.