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: caml-webmasterinriafr
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) ...
.
-
Exceptions communément admises à cette règle
:
-
Définition des composantes d'une paire
:
Au lieu de
let (x, y) = ...
, on peut écrirelet 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
etwith
, tandis que les filtres sont bien délimités par|
et->
.
-
Définition des composantes d'une paire
:
Au lieu de
Écriture des listes
On écritx :: 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, maisx+!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 nonx * (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 écrirex*!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:
-
Corps ferré à gauche dans le cas d'un filtrage («corps» «ferré à gauche»
est une expression de typographie qui signifie que le «corps» du
texte est poussé au maximum vers la marge gauche ``à l'aide du fer'').
let f x = function | C -> | D -> ...;;
Justification: les barres verticales du filtrage s'arrêtent lorsque la définition est finie, on passe encore facilement à la définition suivante.
Critique: Exception désagréable à l'indentation normale.
-
Corps ferré au dessous du nom de la fonction définie.
let f x = let tmp = ... in try g x with | Not_found -> ...;;
Justification: La première ligne de la définition est bien mise en évidence, on passe plus facilement de définition en définition.
Critique: On part trop vite dans la marge droite.
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 etelse if
pour le rappeler.
Encore une fois, choisissez votre style et utilisez-le systématiquement.Critique: Manque d'homogénéité du traitement de toutes les conditions. Pourquoi un cas particulier pour la première condition ?
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 endif 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
ete2
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
etcond
sont petites, maise2
grosse:if cond then e1 else e2si
e1
etcond
sont grosses ete2
petite:if cond then e1 else e2Si toutes les expressions sont grosses:
if cond then e1 else e2S'il y a des délimiteurs
begin end
if cond then begin e1 end else begin e2 endun mélange où
e1
nécessitebegin end
maise2
est petiteif 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 pourmatch
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 parlet
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 delet 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 liaisonlet
.
Au lieu de
List.map (function x -> blabla blabla blabla) lon é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
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 constructionJustification: Terminer la ligne sur l'opération indique bien que l'expression n'est pas terminée.
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 + tLa 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) / uCes 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:
- 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.
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
-
let
avec filtres complexes:
let [x; y] as l = ...
permet de définir simultanément une listel
et les deux éléments qu'elle contientx
ety
. -
let
avec filtre très simple:
let _ = ...
ne définit rien du tout, se contentant en fait d'évaluer l'expression à droite du signe=
.
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.
-
let ... in
: leslet
destructurant qui donnent un avertissement du compilateur seront systématiquement remplacés par un filtrage explicite. Par exemple, au lieu delet [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 écritlet _ = List.map f l in print_newline ()
-
on utilise aussi la fonction prédéfinie
ignore : 'a -> unit
qui ignore son argument pour rendreunit
. On écrit alorsignore (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
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
ettl
qui doit nécessairement être protégé par untry... 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 signifietrue
? 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édicatrecursivep
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 typedé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:
-
É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 bouclewhile
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.
-
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:
-
É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, commelet 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 simplementlet 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''. - 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
ouList.iter
que d'écrire en ligne leurs équivalents en terme de fonction récursive. Pire encore, ne pas utiliserList.map
ouList.iter
mais leur équivalent en ligne, exprimé avecList.fold_right
ouList.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.
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:
-
CTRL-C-CTRL-C
ouMeta-X compile
: lancement de la recompilation depuis l'éditeur (utilise la commandemake
). -
CTRL-X-`
: placement du curseur sur le fichier et à l'endroit exact où le compilateur Caml a signalé une erreur.
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.