Appel aux commentaires
Ceci est un ensemble de conseils raisonnables de présentation des programmes Caml, ainsi que des conseils de programmation qui ont recueilli l'assentiment de programmeurs Caml chevronnés. Cependant, tous les avis circonstanciés sur les éventuelles erreurs ou omissions seront pris en compte avec plaisir. Envoyez vos commentaires ici.
Merci à tous ceux qui ont déjà participé à la critique de cette page:
Daniel de Rauglaudre, Luc Maranget, Jacques Garrigue, Damien Doligez, Xavier Leroy, Bruno Verlyck, Bruno Petazzoni, Francois Maltey, Basile Starynkevitch, Toby Moth, Pierre Lescanne.
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.
En Caml, une part non négligeable de la programmation consiste à définir les types de données adaptés au problème. Cette phase a une importance considérable, même si elle ne donne pas lieu à l'écriture de code exécutable. Elle met en effet en place la logique interne du programme et l'ensemble des invariants que le contrôleur de type de Caml vérifiera pour vous.
On peut donc conseiller une méthodologie en trois points:
Dans l'idéal, une quatrième phase consisterait à prouver formellement les programmes écrits; malheureusement cette étape est souvent hors d'atteinte, faute de temps, de connaissances et même parfois de capacité du programmeur.
Les types de données en Caml sont éminemment riches: on distingue 3 espèces de types, auxquels on peut indépendamment attribuer un genre de visibilité parmi trois 3, ce qui en modifie alors fortement l'usage et donc finalement la nature.
type filename = string;;
type number = | Integer of int | Float of float;;ou encore pour le cas plus simple des types énumérés:
type operator = | Or | And | Not | Imply | Equivalent;;
type person = { firstname : string; lastname : string; };;ou encore pour le cas plus simple des produits cartésiens à champs anonymes:
type point = int * int;;
Les trois genres servent à modéliser différents types de données et à assurer différentes propriétés:
En conclusion: pour une structure libre, utilisez un type concret; pour une structure contrainte, utilisez un type privé; lorsque les différents cas du type doivent rester secrets ou lorsque l'ensemble des opérations possibles sur les valeurs doit être restreint, utilisez un type abstrait.
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!
(1, 2)
, let triplet = (x, y, z) ...
.
let (x, y) = ...
, on peut écrire
let x, y = ...
.
let
et
=
.
match x, y with | 1, _ -> ... | x, 1 -> ... | x, y -> ...
match
et with
, tandis que les filtres
sont bien délimités par |
et ->
.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).
!
» et «.
» ne sont
pas séparés de leurs arguments.)
x + 1
ou x + !y
.
x+1
serait
compris, mais x+!y
changerait de signification puisque
«+!
» est analysé comme un opérateur infixe
multi-symboles.x*y + 2*z
x * z-1
signifie bien (x * z) - 1
,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.(+)
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 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 ...
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:
let ... ;;
let f x = function | C -> | D -> ...;; let g x = let tmp = match x with | C -> | x -> 0 in tmp + 1;;
let f x = function | C -> | D -> ...;;
let f x = let tmp = ... in try g x with | Not_found -> ...;;
let ... in
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
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 ...
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);;
if then else
if cond1 ... if cond2 ... if cond3 ...
if cond1 then e1 else if cond2 then e2 else if cond3 then e3 else e4
if cond then begin e1 end else if cond2 then begin e2 end else if cond3 then ...
else
:
if cond1 ... else if cond2 ... else if cond3 ...
elsif
est un mot clé
dans beaucoup de langages, on utilise donc l'indentation et else
if
pour le rappeler.begin
end
pour ces expressions.
begin
en fin de ligne
if cond then begin e1 end else begin e2 end
begin
en début de ligne:
if cond then begin e1 end else begin e2 end
cond
, e1
et e2
sont
petites on écrit simplement sur une ligne:
if cond then e1 else e2
let .. in
lorsqu'elles sont trop grosses pour tenir
sur la ligne.
let ... in
.
e1
et cond
sont
petites, mais e2
grosse:
if cond then e1 else e2
e1
et cond
sont grosses et
e2
petite:
if cond then e1 else e2
if cond then e1 else e2
begin end
if cond then begin e1 end else begin e2 end
e1
nécessite
begin end
mais e2
est petite
if cond then begin e1 end else e2
if cond then begin e1 end else e2
match
ou try
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" -> ...
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" -> ...
with
, seul sur une ligne, met bien en évidence
l'entrée dans la partie filtrage 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
| Var v -> 1
let rec fib = function | 0 -> 1 | 1 -> 1 | n -> fib (n - 1) + fib ( n - 2);;
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
let
ou
let rec
donnent lieu à plusieurs styles raisonnables qui
respectent les règles générales d'indentations des clauses de filtrage
(sauf la règle concernant les fonctions anonymes
évidemment). Les styles d'indentation préconisés sont décrits dans le
paragraphe concernant les les
définitions globales. Choisissez le style qui vous convient le
mieux, et utilisez toujours le même!
let rec size_lambda accu = function | Abs (x, body) -> size_lambda (succ accu) body | App (lam1, lam2) -> size_lambda (size_lambda accu lam1) lam2 | Var v -> succ accu
let rec size_lambda accu = function | Abs (x, body) -> size_lambda (succ accu) body | App (lam1, lam2) -> size_lambda (size_lambda accu lam1) lam2 | Var v -> succ accu
match
ou function
qui a préalablement été
ferré à droite. N'écrivez pas:
let rec f x = function | [] -> ... ...
let
:
let rec f x = function | [] -> ... ...
->
» des clauses de filtrage.let f = function | C1 -> 1 | Long_name _ -> 2 | _ -> 3;;
let
.
let
est de toute façon nécessaire pour expliciter l'ordre
d'évaluation.let tampon = f x y z «grosse expression» «autre grosse» expression» in ...
let t = «grosse expression» and u = «autre grosse» expression» in let tampon = f x y z t u in ...
let
.
List.map (function x -> blabla blabla blabla) l
let f x = blabla blabla blabla in List.map f l
|
de fin
de ligne représente la marge droite de la ligne):
x + y + z + | t + u |
let in
plutôt que d'avoir à l'indenter
en ligne.
x + y + z + | «grosse | expression» |
let t = «grosse | expression» in | x + y + z + t |
(x + y + z * t) / | («grosse | expression») |
let u = «grosse | expression» in | (x + y + z * t) / u |
let u = «grosse | expression» in | x :: y :: | z + 1 :: t :: u |
Toujours sur le métier remettez votre ouvrage,
Et puis le polissez et le repolissez.
(* 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;;
let f x = assert (x >= 0); ...
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.
int_of_string
,
pas intOfString
).
IntOfString
.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.
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:
1 + 2 * x
signifie 1 + (2 * x)
.
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)
.
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').
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)
1 + 2 * x
signifie 1 + (2 * x)
,
true || false && x
signifie
true || (false && x)
.
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
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 -> ...
if
then
ou else
d'une alternative doit être délimitée:
if cond then begin e1; e2 end else begin e3; e4 end
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.
Éviter les directives open
. Préférer la notation
qualifiée des identificateurs.
On privilégie donc des noms de modules courts mais significatifs.
let lim = String.length name - 1 in ... let lim = Array.length v - 1 in ... ... List.map succ ... ... Array.map succ ...
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);;
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.
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.
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!
| _ -> ...
, mais avec la liste
explicite des constructeurs non examinés dans le reste du filtrage,
par exemple | Cn _ | Cn1 _ -> ...
.
let
destructurantlet
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 = ...
l
et les deux
éléments qu'elle contient x
et y
.
let
avec filtre très simple:let _ = ...
ne définit rien du tout, se contentant
en fait d'évaluer l'expression à droite du signe =
.
let
introduit un
filtrage partiel, on n'a aucun moyen de corriger ce filtrage
dangereux (c'est le cas par exemple pour
let [x; y] as l = List.map succ (l1 @ l2)
qui échouera si le résultat de map
n'a
pas exactement deux éléments (ou si le résultat de map
n'a pas au moins deux éléments si l'on avait écrit
let (x :: y :: _ as l) = List.map succ (l1 @ l2)
).
let
destructurant ne doit pas produire
d'avertissementlet
destructurant que
dans le cas où le filtrage est exhaustif. Dans ce cas, on dit que le
filtre est irréfutable. Typiquement, on se limitera donc à des
définitions de type produits (n-uplets ou enregistrements) ou à des
définitions dont le filtre appartient à un type somme à un seul
cas. Dans tous les autres cas, on préfèrera une construction
match ... with
explicite.
let ... in
: les let
destructurant qui
donnent un avertissement du compilateur seront systématiquement
remplacés par un filtrage explicite. Par exemple, au lieu de
let [x; y] as l = List.map succ (l1 @ l2) in expression
,
on écrira:
match List.map succ (l1 @ l2) with | [x; y] as l -> expression | _ -> assert false
let x, y, l = match List.map succ (l1 @ l2) with | [x; y] as l -> x, y, l | _ -> assert false
let
destructurant généraux.let _ = ...
List.map f l; print_newline ()
let _ = List.map f l in print_newline ()
ignore : 'a ->
unit
qui ignore son argument pour rendre unit
.
On écrit alors
ignore (List.map f l); print_newline ()
List.iter f l; print_newline ()
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
.
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.
e1; e2; e3
à
let _ = e1 in let _ = e2 in e3
hd
et tl
On n'utilisera pas les fonctions hd
et
tl
mais un filtrage explicite de la liste argument.
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.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;;
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.
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.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
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!).L'une des grandes forces de Caml réside dans la puissance des structures de données définissables et la simplicité de leur manipulation. Il faut donc en profiter au maximum et ne pas hésiter à définir ses propres structures de données. En particulier, il ne faut pas représenter systématiquement des énumérations par des entiers, ni des énumérations à deux cas par des booléens. Exemples:
type figures = | Triangle | Carré | Cercle | Parallélogramme;; type convexité = | Convexe | Concave | Quelconque;; type type_de_définition = | Récursive | Non_récursive;;
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 ?type_de_définition
, on définirait simplement un prédicat
recursivep
qui rend vrai si la définition est
récursive.| Let of bool * string * expression
d'un type
définition
; un cas de filtrage typique sur une
définition aura l'allure suivante:
| Let (_, v, e) as def -> if recursivep def then ``code pour le cas récursif'' else ``code pour le cas non récursif''on encore, si le prédicat
recursivep
s'applique
directement aux booléens:
| Let (b, v, e) -> if recursivep b then ``code pour le cas récursif'' else ``code pour le cas non récursif''En revanche, avec un type explicite on écrirait plutôt:
| Let (Récursive, v, e) -> ``code pour le cas récursif'' | Let (Non_récursive, v, e) -> ``code pour le cas non récursif''la différence est faible il est vrai, et l'on peut préférer l'un ou l'autre style selon ses goûts. Remarquez cependant que la méthode avec type binaire explicitement défini n'empêche nullement de définir un prédicat pour se ramener au cas utilisant directement des booléens. Note finale: dans le cas de l'utilisation directe des booléens, la définition d'un prédicat n'empêche pas un autre programmeur d'utiliser directement les booléens (en écrivant par exemple
| Let (b, v, e) -> if b then ...
ce qui est bien typé et tombe dans le travers de sémantique peu
claire dénoncé plus haut, ce que le langage interdit lorsqu'on définit
un type somme énuméré).
A contrario, il ne faut pas systématiquement définir des drapeaux
booléens à l'aide d'un nouveau type, lorsque
l'interprétation des constructeurs true
et
false
paraît claire. On peut ainsi discuter de l'utilité
des types suivants:
type switch = | On | Off;; type bit = | One | Zero;;
La même objection est recevable pour les types énumérés représentés par des entiers, lorsque les entiers ont une interprétation évidente pour les données à représenter.
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.
Les itérateurs de Caml sont un trait puissant et utile. Il ne faut cependant pas en abuser, ni a contrario les négliger: il vous sont fournis par les bibliothèques et ont toutes les chances d'être corrects et mûrement réfléchis par l'auteur de la bibliothèque. Il est donc inutile de les réinventer.
On écrira donc
let carre_elements elements = map carre elements;;
plutôt que:
let rec carre_elements = function | [] -> [] | elem :: elements -> carre elem :: carre_elements elements;;
En revanche, on évitera d'écrire:
let iterator f x l = List.fold_right (List.fold_left f) [List.map x l] l;;
Quoiqu'on obtienne:
val iterator : ('a list -> 'a -> 'a list) -> ('a -> 'a) -> 'a list -> 'a list = <fun> # iterator (fun l x -> x :: l) (fun l -> List.rev l) [[1; 2; 3]] ;; - : int list list = [[1; 2; 3]; [3; 2; 1]]
En cas de besoin exprès, on aura soin d'ajouter un commentaire explicatif: à mon avis il s'impose!
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.
Les classes d'Objective Caml s'imposent lorsqu'on a besoin d'héritage, c'est-à-dire d'un raffinement incrémental des données et de leurs fonctionnalités.
Les types de données conventionnels (types somme en particulier) s'imposent lorsqu'on a besoin de filtrage.
Les modules s'imposent lorsque les structures de données sont fixes et que leur fonctionnalité est également fixée ou que l'ajout de nouvelles fonctions dans les programmes qui les utilisent est suffisant.
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.
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).
for
pour parcourir un
vecteur, mais au contraire d'utiliser une boucle while
complexe, avec une ou des références (trop d'affectations inutiles et
trop d'occasions d'erreur).mutable
dans les définitions de types enregistrement devrait être implicite.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.mutable
dans les définitions de types enregistrement devrait être supprimé de
Caml.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;;
let ... in
par l'application
d'une fonction anonyme à un argument. On écrirait(fun x y -> x + y) e1 e2au lieu d'écrire simplement
let x = e1 and y = e2 in x + y
let in
est assez ``populaire''.
let flush_ps () = if not !psused then psused := true;;ou (plus subtile encore)
let sync b = if !last_is_dvi <> b then begin last_is_dvi := b end;;
List.map
ou List.iter
que d'écrire en ligne leurs équivalents en
terme de fonction récursive. Pire encore, ne pas utiliser
List.map
ou List.iter
mais leur équivalent
en ligne, exprimé avec List.fold_right
ou
List.fold_left
.
(fun u -> print_string "world"; print_string u) (let temp = print_string "Hello"; "!" in ((fun x -> print_string x; flush stdout) " "; temp));;
print_string "Hello world!"
, vous pouvez sans doute
soumettre vos oeuvres au concours du code Caml le plus
illisible.
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.
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
ou Meta-X compile
:
lancement de la recompilation depuis l'éditeur (utilise la commande
make
).
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
.
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.
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.
L'utilitaire make
est indispensable pour gérer la
compilation et la recompilation des programmes. Des modèles de
fichiers make
sont disponibles pour Caml Light et Objective Caml. On peut aussi
consulter les Makefiles
des compilateurs Caml.
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
).
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.
Contacter l'auteur Pierre.Weis@inria.fr