Utilisateur:Regards sur sciences/agreg/fiches/fiche sur OCaml
exemples de code
[modifier | modifier le wikicode]$ ocaml
Objective Caml version 3.09.0
#
Code can then be entered at the "#" prompt. For example, to calculate 1+2*3:
# 1 + 2 * 3;;
- : int = 7
OCaml infers the type of the expression to be "int" (a machine-precision integer) and gives the result "7".
Hello World
[modifier | modifier le wikicode]The following program "hello.ml":
print_endline "Hello World!"
can be compiled into a bytecode executable:
$ ocamlc hello.ml -o hello
or compiled into an optimized native-code executable:
$ ocamlopt hello.ml -o hello
and executed:
$ ./hello
Hello World!
$
The first argument to ocamlc, "hello.ml", specifies the source file to compile and the "-o hello" flag specifies the output file[1].
Summing a list of integers
[modifier | modifier le wikicode]Lists are one of the fundamental datatypes in OCaml. The following code example defines a recursive function sum that accepts one argument, integers, which is supposed to be a list of integers. Note the keyword rec
which denotes that the function is recursive. The function recursively iterates over the given list of integers and provides a sum of the elements. The match statement has similarities to C's switch element, though it is far more general.
let rec sum integers = (* Keyword rec means 'recursive'. *)
match integers with
| [] -> 0 (* Yield 0 if integers is the empty
list []. *)
| first :: rest -> first + sum rest;; (* Recursive call if integers is a non-
empty list; first is the first
element of the list, and rest is a
list of the rest of the elements,
possibly []. *)
# sum [1;2;3;4;5];;
- : int = 15
Another way is to use standard fold function that works with lists.
let sum integers =
List.fold_left (fun accumulator x -> accumulator + x) 0 integers;;
# sum [1;2;3;4;5];;
- : int = 15
Since the anonymous function is simply the application of the + operator, this can be shortened to:
let sum integers =
List.fold_left (+) 0 integers
Furthermore, one can omit the list argument by making use of a partial application:
let sum =
List.fold_left (+) 0
Quicksort
[modifier | modifier le wikicode]OCaml lends itself to concisely expressing recursive algorithms. The following code example implements an algorithm similar to quicksort that sorts a list in increasing order.
let rec qsort = function
| [] -> []
| pivot :: rest ->
let is_less x = x < pivot in
let left, right = List.partition is_less rest in
qsort left @ [pivot] @ qsort right
Birthday problem
[modifier | modifier le wikicode]The following program calculates the smallest number of people in a room for whom the probability of completely unique birthdays is less than 50% (the birthday problem, where for 1 person the probability is 365/365 (or 100%), for 2 it is 364/365, for 3 it is 364/365 × 363/365, etc.) (answer = 23).
let year_size = 365.
let rec birthday_paradox prob people =
let prob = (year_size -. float people) /. year_size *. prob in
if prob < 0.5 then
Printf.printf "answer = %d\n" (people+1)
else
birthday_paradox prob (people+1)
;;
birthday_paradox 1.0 1
Church numerals
[modifier | modifier le wikicode]The following code defines a Church encoding of natural numbers, with successor (succ) and addition (add). A Church numeral Modèle:OCaml is a higher-order function that accepts a function Modèle:OCaml and a value Modèle:OCaml and applies Modèle:OCaml to Modèle:OCaml exactly Modèle:OCaml times. To convert a Church numeral from a functional value to a string, we pass it a function that prepends the string Modèle:OCaml to its input and the constant string Modèle:OCaml.
let zero f x = x
let succ n f x = f (n f x)
let one = succ zero
let two = succ (succ zero)
let add n1 n2 f x = n1 f (n2 f x)
let to_string n = n (fun k -> "S" ^ k) "0"
let _ = to_string (add (succ two) two)
Arbitrary-precision factorial function (libraries)
[modifier | modifier le wikicode]A variety of libraries are directly accessible from OCaml. For example, OCaml has a built-in library for arbitrary-precision arithmetic. As the factorial function grows very rapidly, it quickly overflows machine-precision numbers (typically 32- or 64-bits). Thus, factorial is a suitable candidate for arbitrary-precision arithmetic.
In OCaml, the Num module (now superseded by the ZArith module) provides arbitrary-precision arithmetic and can be loaded into a running top-level using:
# #use "topfind";;
# #require "num";;
# open Num;;
The factorial function may then be written using the arbitrary-precision numeric operators Modèle:Mono, Modèle:Mono and Modèle:Mono :
# let rec fact n =
if n =/ Int 0 then Int 1 else n */ fact(n -/ Int 1);;
val fact : Num.num -> Num.num = <fun>
This function can compute much larger factorials, such as 120!:
# string_of_num (fact (Int 120));;
- : string =
"6689502913449127057588118054090372586752746333138029810295671352301633
55724496298936687416527198498130815763789321409055253440858940812185989
8481114389650005964960521256960000000000000000000000000000"
Triangle (graphics)
[modifier | modifier le wikicode]The following program renders a rotating triangle in 2D using OpenGL:
let () =
ignore (Glut.init Sys.argv);
Glut.initDisplayMode ~double_buffer:true ();
ignore (Glut.createWindow ~title:"OpenGL Demo");
let angle t = 10. *. t *. t in
let render () =
GlClear.clear [ `color ];
GlMat.load_identity ();
GlMat.rotate ~angle: (angle (Sys.time ())) ~z:1. ();
GlDraw.begins `triangles;
List.iter GlDraw.vertex2 [-1., -1.; 0., 1.; 1., -1.];
GlDraw.ends ();
Glut.swapBuffers () in
GlMat.mode `modelview;
Glut.displayFunc ~cb:render;
Glut.idleFunc ~cb:(Some Glut.postRedisplay);
Glut.mainLoop ()
The LablGL bindings to OpenGL are required. The program may then be compiled to bytecode with:
$ ocamlc -I +lablGL lablglut.cma lablgl.cma simple.ml -o simple
or to nativecode with:
$ ocamlopt -I +lablGL lablglut.cmxa lablgl.cmxa simple.ml -o simple
or, more simply, using the ocamlfind build command
$ ocamlfind opt simple.ml -package lablgl.glut -linkpkg -o simple
and run:
$ ./simple
Far more sophisticated, high-performance 2D and 3D graphical programs can be developed in OCaml. Thanks to the use of OpenGL and OCaml, the resulting programs can be cross-platform, compiling without any changes on many major platforms.
Fibonacci sequence
[modifier | modifier le wikicode]The following code calculates the Fibonacci sequence of a number n inputted. It uses tail recursion and pattern matching.
let fib n =
let rec fib_aux m a b =
match m with
| 0 -> a
| _ -> fib_aux (m - 1) b (a + b)
in fib_aux n 0 1
Higher-order functions
[modifier | modifier le wikicode]Functions may take functions as input and return functions as result. For example, applying twice to a function f yields a function that applies f two times to its argument.
let twice (f : 'a -> 'a) = fun (x : 'a) -> f (f x);;
let inc (x : int) : int = x + 1;;
let add2 = twice inc;;
let inc_str (x : string) : string = x ^ " " ^ x;;
let add_str = twice(inc_str);;
# add2 98;;
- : int = 100
# add_str "Test";;
- : string = "Test Test Test Test"
The function twice uses a type variable 'a to indicate that it can be applied to any function f mapping from a type 'a to itself, rather than only to int->int functions. In particular, twice can even be applied to itself.
# let fourtimes f = (twice twice) f;;
val fourtimes : ('a -> 'a) -> 'a -> 'a = <fun>
# let add4 = fourtimes inc;;
val add4 : int -> int = <fun>
# add4 98;;
- : int = 102
conseils de programmation
[modifier | modifier le wikicode]Conseils généraux pour l'écriture
[modifier | modifier le wikicode]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
[modifier | modifier le wikicode]Conventions lexicales
Pseudo loi des espaces: n'hésitez pas à séparer les mots de vos programmes à l'aide de blancs; la touche d'espacement est la plus facile à trouver, il n'y a pas de raison de s'en priver!
Délimiteurs Les symboles délimiteurs sont suivis d'un espace, les symboles d'opérations sont entourés d'espaces. Ce fut un grand progrès de la typographie de séparer les mots par des blancs pour rendre les textes écrits plus faciles à lire. Faites de même dans vos programmes si vous voulez des textes lisibles. Écriture des paires Un n-uplet est parenthésé et la ou les virgules (délimiteurs) sont suivies d'un espace: (1, 2), let triplet = (x, y, z) ....
Exceptions communément admises à cette règle : Définition des composantes d'une paire : Au lieu de let (x, y) = ..., on peut écrire let x, y = ....
Justification: il s'agit de la définition simultanée de plusieurs valeurs, non de la construction d'un n-uplet. En outre, le filtre est bien délimité entre let et =.
Filtrage simultané de plusieurs valeurs : On admet d'omettre les parenthèses des n-uplets dans le cas d'un filtrage simultané sur plusieurs valeurs.
match x, y with | 1, _ -> ... | x, 1 -> ... | x, y -> ...
Justification: Il s'agit d'un filtrage en parallèle sur plusieurs valeurs, non de la construction d'un n-uplet. En outre, les expressions à filtrer sont délimitées par match et with, tandis que les filtres sont bien délimités par | et ->.
Écriture des listes On écrit x :: l avec des espaces autour du symbole :: (car :: est un opérateur infixe donc entouré d'espaces) et [1; 2; 3] (car ; est un délimiteur donc suivi d'un espace). Écriture des symboles d'opérations On aura soin de bien séparer les symboles d'opération par des espaces: non seulement les formules sont plus lisibles, mais les confusions d'opérateurs multi-symboles sont évitées. (Exceptions évidentes à cette règle, les symboles «!» et «.» ne sont pas séparés de leurs arguments.) Exemple: on écrit x + 1 ou x + !y.
Justification: Si l'on écrivait sans espaces, alors x+1 serait compris, mais x+!y changerait de signification puisque «+!» est analysé comme un opérateur infixe multi-symboles.
Critique: L'absence d'espaces autour d'un opérateur améliore la lisibilité des formules quand on l'utilise pour refléter les priorités relatives des opérateurs. Par exemple x*y + 2*z met bien en évidence la priorité de la multiplication sur l'addition.
Réponse à la critique: Cette idée est séduisante mais elle est illusoire, parce que rien dans le langage ne donne l'assurance que les espaces reflètent bien la signification de la formule. Par exemple x * z-1 signifie bien (x * z) - 1, et non x * (z - 1) comme l'interprétation proposée des espaces semblerait le suggérer. D'autre part, le problème des opérateurs multi-symboles empêcherait d'utiliser cette convention de façon uniforme: on ne pourrait pas restreindre les espaces autour de la multiplication pour écrire x*!y + 2*!z. Enfin ce jeu sur les espaces est une convention ténue et subtile, un message sub-liminal difficile à saisir à la lecture: si vous voulez mettre en évidence les priorités, utilisez le moyen significatif que vous procure le langage: mettez des parenthèses.
Autre justification: Le cas des opérateurs infixes n'est plus un cas particulier compliqué; en effet, si l'on peut écrire (+) sans blancs, on ne peut pas écrire (*) puisque (* est évidemment compris comme le début d'un commentaire. Il faut donc écrire au minimum ( *), mais un blanc supplémentaire après * est vraiment préférable pour éviter aussi que *) ne soit compris, dans certains contextes, comme une fin de commentaire. Toutes ces difficultés sont évitées simplement en adoptant la règle simple de toujours mettre des blancs autour des opérateurs. En fait, cette règle n'est pas si difficile à suivre: la barre d'espace est la plus grosse touche du clavier, c'est la plus facile à taper et vous ne pouvez pas la manquer!
Écriture des longues chaînes de caractères On indentera les longues chaînes de caractères sur plusieurs lignes, en utilisant la convention de passage à la ligne (un caractère \ en fin de ligne) qui omet les blancs présents au début de la ligne suivante:
let déclaration_universelle = "-1- Les logiciels naissent et demeurent libres et égaux en droit;\n\ les distinctions ne peuvent être fondées que sur l'utilité commune." in ...
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 et else if pour le rappeler.
Critique: Manque d'homogénéité du traitement de toutes les conditions. Pourquoi un cas particulier pour la première condition ?
Encore une fois, choisissez votre style et utilisez-le systématiquement. Alternatives simples
Plusieurs styles sont possibles pour les alternatives simples, selon la taille des expressions en jeu et surtout la présence de délimiteurs begin end pour ces expressions.
En cas de délimitation des branches de l'alternative, plusieurs styles sont préconisés:
style begin en fin de ligne: if cond then begin e1 end else begin e2 end
if cond then begin e1 end else begin e2 end
ou encore premier begin en début de ligne:
if cond then begin e1 end else begin e2 end
En fait l'indentation des conditionnelles dépend de la taille des expressions qui les composent.
Si cond, e1 et e2 sont petites on écrit simplement sur une ligne:
if cond then e1 else e2
Si les expressions qui composent l'alternative sont purement fonctionnelles (ne font pas d'effets), on s'attachera à les lier par un let .. in lorsqu'elles sont trop grosses pour tenir sur la ligne.
Justification: On retrouve alors l'indentation simple sur une ligne qui est la plus lisible. Accessoirement, le nommage aide à la relecture.
On considère donc maintenant le cas où les expressions considérées font des effets, ce qui empêche de les lier simplement par un let ... in.
si e1 et cond sont petites, mais e2 grosse:
if cond then e1 else e2
si e1 et cond sont grosses et e2 petite:
if cond then e1 else e2
Si toutes les expressions sont grosses:
if cond then e1 else e2
S'il y a des délimiteurs begin end
if cond then begin e1 end else begin e2 end
un mélange où e1 nécessite begin end mais e2 est petite
if cond then begin e1 end else e2
Indentation des filtrages Principes généraux Les clauses de filtrages sont toutes introduites par une barre verticale y compris la première.
Critique: La première barre est facultative, inutile de l'écrire.
Réponse à la critique: L'indentation ainsi obtenue paraît non naturelle : le premier cas est forcément plus indenté que le passage normal à la ligne ne l'exigerait. C'est donc une exception inutile à la règle d'indentation. C'est aussi une exception à la syntaxe utilisée pour toutes les autres clauses. Enfin, c'est esthétiquement douteux (certains disent même laid).
Les clauses de filtrages seront toutes alignées au niveau de la barre verticale qui commence chaque clause.
Si l'expression correspondant à une clause ne tient pas sur la ligne, on ira à la ligne immédiatement après la flèche de la clause (qui terminera donc la ligne), puis l'on indentera normalement, à partir du début du filtre correspondant.
Les flèches des clauses n'ont pas à être alignées. match ou try Pour un match ou un try les clauses seront alignées sur le début de la construction:
match lam with | Abs (x, body) -> 1 + size_lambda body | App (lam1, lam2) -> size_lambda lam1 + size_lambda lam2 | Var v -> 1
try f x with | Not_found -> ... | Failure "not yet implemented" -> ...
Le mot clé with sera placé en fin de ligne. En cas de débordement de l'expression qui le précède, with apparaîtra seul sur une ligne:
try let y = f x in if ... with | Not_found -> ... | Failure "not yet implemented" -> ...
Justification: Le mot clé with, seul sur une ligne, met bien en évidence l'entrée dans la partie filtrage de la construction.
Indentation des expressions des clauses Si l'expression située à droite d'une flèche de filtrage ne tient pas sur la ligne, on ira à la ligne immédiatement après la flèche.
match lam with | Abs (x, body) -> 1 + size_lambda body | App (lam1, lam2) -> size_lambda lam1 + size_lambda lam2 | Var v -> 1
Certains programmeurs prétendent que cette règle doit être appliquée à toutes les clauses, dès que l'une des expressions déborde. Ils indenteraient donc la dernière clause du filtrage précédent ainsi:
| Var v -> 1
D'autres vont encore plus loin en appliquant cette règle systématiquement à toutes les clauses filtrages.
let rec fib = function | 0 -> 1 | 1 -> 1 | n -> fib (n - 1) + fib ( n - 2);;
Critique: Manque de compacité du code; pour des filtrages simples (ou des clauses simples dans des filtrages complexes), cette règle n'apporte rien à la lisibilité.
Justification: Je ne vois pas de justification raisonnable, sauf si vous êtes payé à la ligne de code: adopter cette règle est alors un bon moyen d'écrire plus de lignes de code Caml, sans risquer d'introduire des bugs supplémentaires.
Filtrages dans les fonctions anonymes Comme pour match ou try, les filtrages des fonctions anonymes, introduits par function, sont alignés sur le mot-clé function:
map (function | Abs (x, body) -> 1 + size_lambda 0 body | App (lam1, lam2) -> size_lambda (size_lambda 0 lam1) lam2 | Var v -> 1) lambda_list
Filtrages dans les fonctions nommées Les filtrages des fonctions définies par let ou let rec donnent lieu à plusieurs styles raisonnables qui respectent les règles générales d'indentations des clauses de filtrage (sauf la règle concernant les fonctions anonymes évidemment). Voir plus haut pour les styles recommandés.
let rec size_lambda accu = function | Abs (x, body) -> size_lambda (succ accu) body | App (lam1, lam2) -> size_lambda (size_lambda accu lam1) lam2 | Var v -> succ accu
let rec size_lambda accu = function | Abs (x, body) -> size_lambda (succ accu) body | App (lam1, lam2) -> size_lambda (size_lambda accu lam1) lam2 | Var v -> succ accu
Mauvaise indentation des filtrages Pas d'indentation sauvage des fonctions et des analyses de cas. Cela consiste à indenter directement sous le mot-clé match ou function qui a préalablement été ferré à droite. N'écrivez pas:
let rec f x = function | [] -> ... ...
mais indentez plutôt la ligne au mot-clé let:
let rec f x = function | [] -> ... ...
Justification: Avec l'indentation sauvage, on se cogne dans la marge et l'esthétique est douteuse ...
Pas non plus d'alignement sauvage des «->» des clauses de filtrage. On considère comme de mauvaise pratique de soigneusement placer toutes les flèches d'un filtrage au même taquet de tabulation, comme dans le code suivant:
let f = function | C1 -> 1 | Long_name _ -> 2 | _ -> 3;;
Justification: L'indentation sauvage des flèches se traduit par une exception désagréable à l'indentation normale; cela rend plus difficile la maintenance du programme (l'ajout d'un cas supplémentaire peut amener à modifier l'indentation de toutes les lignes ou bien ... l'abandon pur et simple de l'alignement des flèches; mieux vaut donc commencer directement par là et ne pas les aligner du tout!).
Indentation des appels de fonctions Indentation to the function's name: Indentation calée au nom de la fonction: Le problème ne se pose que pour les fonctions ayant beaucoup d'arguments ou bien des arguments très complexes qui ne peuvent tenir sur la même ligne. On s'efforcera d'indenter les expressions par rapport au nom de la fonction (1 ou 2 espaces selon la convention adoptée). Les petits arguments seront écrits sur la même ligne et l'on changera de ligne au début d'un argument.
Dans toute la mesure du possible on évitera les arguments constitués d'une expression complexe: dans ce cas on définira le «gros» argument par une construction let.
Justification: Le problème d'indentation disparaît; en outre, si le nom donné aux expressions est significatif, le code est bien plus lisible.
Justification supplémentaire: Si l'évaluation des arguments produit des effets, la liaison let est de toute façon nécessaire pour expliciter l'ordre d'évaluation.
Nommage des arguments complexes: Au lieu de
let temp = f x y z ``large expression ``other large expression in ...
on écrira
let t = ``large expression and u = ``other large expression in let temp = f x y z t u in ...
Nommage des fonctions immédiates: En cas d'itérateur dont l'argument est une fonction complexe, on définit aussi la fonction par une liaison let. Au lieu de
List.map (function x -> blabla blabla blabla) l
on écrira
let f x = blabla blabla blabla in List.map f l
Justification: C'est bien plus clair, surtout si le nom donné à la fonction est significatif.
Indentation des opérations Lorsqu'une opération comporte des arguments complexes, ou en présence de multiples appels à la même opération, l'on terminera la ligne à l'opérateur, et l'on n'indentera pas le reste des opérations. Par exemple (ici la barre verticale | de fin de ligne représente la marge droite de la ligne):
x + y + z + t + u
Justification: Terminer la ligne sur l'opération indique bien que l'expression n'est pas terminée.
En cas de «grosse expression» dans une telle suite d'opération, on préfèrera encore une fois définir la «grosse expression» à l'aide d'une construction let in plutôt que d'avoir à l'indenter en ligne. Au lieu de
x + y + z + «grosse expression»
on écrira
let t = «grosse expression» in x + y + z + t
La liaison des expressions trop grosses pour être écrites dans une opération s'impose franchement en cas de combinaison d'opérateurs. Au lieu de l'expression peu lisible
(x + y + z * t) / (``large expression)
on écrira
let u = ``large expression in (x + y + z * t) / u
Ces conseils s'étendent à tous les opérateurs. Par exemple:
let u = ``large expression in x :: y :: z + 1 :: t :: u
Conseils de programmation Méthode de programmation
Toujours sur le métier remettez votre ouvrage, Et puis le polissez et le repolissez.
Programmez simple et clair Quand c'est fini, relisez, simplifiez et clarifiez. À toutes les étapes de la création, utilisez votre tête! Découpez vos programmes en petites fonctions Les petites fonctions sont plus faciles à maîtriser. Mettez en facteur les morceaux de code répétés en les définissant dans des fonctions séparées Le partage du code ainsi obtenu facilite la maintenance puisque toute correction ou amélioration est automatiquement répercutée. En outre, le simple fait d'isoler et de nommer un morceau de code permet quelquefois d'identifier une fonctionnalité insoupçonnée. N'utilisez pas le copier-coller en programmant Coller du code signifie à coup sûr l'introduction d'un défaut de partage du code du programme et une négligence à identifier une fonction auxiliaire utile; donc cela signifie qu'on perd du partage dans le code du programme. La perte de partage de code conduit à des difficultés dans la maintenance: une erreur dans le code copié doit être corrigé à chaque occurrence du bug dans chacune des copies!
De surcroît, il est difficile de reconnaître les mêmes 10 lignes de code repétées 20 fois dans un programme. En revanche, si ces 10 lignes sont définies par une fonction auxiliaire, il n'y a aucun problème à savoir où ces lignes sont utilisées: ce sont tous les sites d'appel de la fonction auxiliaire. Couper-coller du code rend donc les programmes plus difficiles à comprendre.
En conclusion, copier-coller du code produit des programmes plus difficiles à lire, à comprendre et à maintenir: il faut absolument l'éviter. Commentaires dans les programmes N'hésitez pas à commenter lorsqu'il y a une difficulté S'il n'y a pas de difficulté, il n'y a pas lieu de commenter Évitez les commentaires dans le corps des fonctions Préférez un commentaire au début de la fonction... qui explique le fonctionnement de l'algorithme utilisé. Encore une fois, s'il n'y a pas de difficulté, il n'y a pas lieu de commenter. Évitez les commentaires nocifs Un commentaire nocif est un commentaire qui n'apporte aucune valeur ajoutée, c'est-à-dire aucune information autre que triviale. Le commentaire nocif n'a évidemment pas d'intérêt; il est même dangereux car il distrait inutilement le lecteur. On l'utilise souvent pour remplir des critères dits de métrologie du logiciel, par exemple le rapport (nombre de lignes de code) / (nombre de lignes de programme) qui mesure parfaitement un rapport dont j'ignore l'intérêt. Proscrire absolument les commentaires nocifs.
Voici un bel exemple, car les mots employés sont techniques et le commentaire passe ainsi facilement pour un vrai commentaire quand en fait son contenu est vide:
(* Fonction print_lambda: imprime une lambda-expression passée en argument.
Arguments: lam, une lambda expression quelconque. Résultats: aucun.
Remarque: print_lambda ne peut être utilisée que pour son effet. *) let rec print_lambda lam = match lam with | Var s -> printf "%s" s | Abs l -> printf "\\ %a" print_lambda l | App (l1, l2) -> printf "(%a %a)" print_lambda l1 print_lambda l2;;
Mode d'emploi dans l'interface du module. Le mode d'emploi de la fonction doit apparaître dans l'interface du module qui l'exporte, pas forcément dans le programme qui l'implémente. On choisira des commentaires à la manière des interfaces de modules du système Caml, ce qui permettra ensuite d'extraire automatiquement la documentation de l'interface du module si besoin est. Utilisez les assertions
Les assertions permettent d'éviter les commentaires verbeux, tout en permettant une vérification utile à l'exécution.
Par exemple, les conditions de validité des arguments de fonction sont utilement vérifiées par des assertions.
let f x = assert (x >= 0); ...
Notez en outre qu'une assertion est souvent préférable à un commentaire car elle est plus fiable: une assertion est pertinente car elle est vérifiée à chaque exécution (même si le programme est compilé ordinairement avec l'option du compilateur qui omet les assertions, il est simple de remettre l'option de vérification en cas de comportement étrange du programme); au contraire, un commentaire n'a aucun contenu sémantique et peut donc facilement devenir obsolète: au lieu d'être une aide, il se transforme alors souvent en un obstacle à la bonne compréhension du programme. Commentaires ligne à ligne du code impératif
Bien sûr, lorsqu'on écrit du code très difficile, et particulièrement en cas de code avec beaucoup d'effets mémoire (modifications physiques de structures de données), il est quelquefois indispensable de commenter dans le corps des fonctions, pour expliquer en quoi le programme implémente réellement l'algorithme décrit dans l'entête de la fonction, ou encore pour suivre l'évolution des invariants que la fonction doit maintenir. Encore une fois, lorsqu'il y a des difficultés il faut absolument commenter, à chaque ligne si nécessaire. Choix des identificateurs
Il est difficile de choisir des identificateurs dont le nom évoque la signification du morceau de programme correspondant. C'est pourquoi on y attachera un soin particulier, en privilégiant la clarté et la régularité du nommage. Ne pas utiliser d'abréviations pour les noms globaux
Les identificateurs globaux (y compris et surtout ceux des fonctions) peuvent être longs, car il importe de comprendre à quoi ils servent loin de leur définition. Séparer les mots par des soulignés (int_of_string, pas intOfString)
Les changements de casse ont une forte portée sémantique dans les programmes Caml: les mots commençant par des majuscules sont réservés aux constructeurs et aux noms de modules en OCaml. De surcroît, les noms de valeurs globales doivent obligatoirement commencer par une minuscule. Ces règles empêchent d'employer la convention de façon cohérente: le premier mot doit forcément commencer par une minuscule et il est interdit d'appeler une fonction IntOfString. Toujours nommer du même nom les arguments de fonctions qui ont la même signification
Si nécessaire, on explicitera ce nommage dans un commentaire en tête de fichier); s'il y a plusieurs arguments de même signification on les suffixera par des numéros. Les identificateurs locaux peuvent être brefs, et doivent être repris d'une fonction à l'autre
Cela augmente la régularité du style. Éviter d'utiliser des identificateurs dont l'aspect peut prêter à confusion comme l ou O, faciles à confondre avec 1 et 0.
Exemple:
let add_expression expr1 expr2 = ... let print_expression expr = ...
On tolèrera une exception à la recommandation de ne pas utiliser les changements de casse pour séparer les mots des identificateurs dans le cas d'interfaçage avec des bibliothèques existantes qui utilisent cette convention de nommage: cela permet aux utilisateurs Caml de la bibliothèque de se repérer plus aisément dans la documentation originale de la bibliothèque. Utilisation des parenthèses dans les expressions
Les parenthèses sont significatives: elles indiquent la nécessité d'utiliser une priorité inhabituelle. Elles doivent donc être utilisées à bon escient et non pas saupoudrées au hasard dans les programmes. Pour cela, vous devez connaître les priorités habituelles, c'est-à-dire les combinaisons d'opérations qui ne nécessitent pas de parenthèses. Fort heureusement ce n'est pas compliqué pour qui connaît un peu de mathématiques ou s'efforce de suivre les règles suivantes: Opérateurs arithmétiques: mêmes règles qu'en mathématique
Par exemple: 1 + 2 * x means 1 + (2 * x). Applications de fonctions: mêmes règles qu'en mathématique dans l'usage des fonctions trigonométriques
En mathématique, on écrit sin x pour signifier sin (x). De même sin x + cos x signifie (sin x) + (cos x) pas sin (x + (cos x)). On utilise les même conventions en Caml: on écrit f x + g x pour signifier (f x) + (g x). Cette convention est généralisée à tous les opérateurs (infixes): f x :: g x signifie (f x) :: (g x), f s ^ g s' signifie (f s) ^ (g s'), failwith s ^ s' signifie (failwith s) ^ s' et pas failwith (s ^ s'). Comparaisons et opérateurs booléens
Les comparaisons sont des opérateurs infixes, donc les règles précédentes s'appliquent. C'est pourquoi f x < g x signifie (f x) < (g x). Pour des raisons de typage (pas d'autre interprétation sensée), l'expression f x < x + 2 signifie (f x) < (x + 2). De même f x < x + 2 && x > 3 signifie ((f x) < (x + 2)) && (x > 3). Les priorités relatives des opérateurs booléens sont celles des mathématiques
Bien que les mathématiciens aient tendance à abuser des parenthèses dans ce cas, l'opérateur ``ou des booléens est analogue à l'addition et le ``et à la multiplication. Donc, de même que 1 + 2 * x signifie 1 + (2 * x), true || false && x signifie true || (false && x). Délimitation des constructions dans les programmes
Lorsqu'il est nécessaire de délimiter des constructions syntaxiques dans les programmes, on utilise comme délimiteurs les mots-clés begin et end plutôt que des parenthèses. Cependant, l'utilisation de parenthèses est acceptable si vous le faites de façon cohérente, c'est-à-dire systématique.
Cette délimitation explicite des constructions concerne essentiellement deux constructions: les filtrages imbriqués et les séquences imbriquées dans les constructions if then else. match dans un match
Lorsqu'une construction match ... with ou try ... with apparaît dans une clause de filtrage, il faut impérativement délimiter cette construction imbriquée (sinon les clauses du filtrage englobant sont automatiquement associées au filtrage englobé). Par exemple:
match x with | 1 ->
begin match y with | ... end
| 2 -> ...
Séquences dans les branches d'un if
De même une séquence qui apparaît dans la partie then ou else d'une alternative doit être délimitée:
if cond then begin
e1; e2
end else begin
e3; e4
end
Utilisation des modules Découpage en modules
Il faut découper les programmes en modules cohérents.
Pour chaque module, il faut écrire une interface explicite.
Pour chaque interface, il faut écrire la documentation des entités définies par le module, fonctions, types, exceptions, etc. Ouverture des modules
Éviter les directives open. Préférer la notation qualifiée des identificateurs. On privilégie donc des noms de modules courts mais significatifs.
Justification: L'utilisation d'identificateurs non qualifiés est ambiguë et donne lieu à des erreurs sémantiques difficiles à détecter.
let lim = String.length name - 1 in ...
let lim = Array.length v - 1 in ...
... List.map succ ...
... Array.map succ ...
Modules utilisés ouverts, modules utilisés fermés
On considère qu'il est normal d'ouvrir un module qui modifie l'environnement, et apporte d'autres versions d'un ensemble important de fonctions. Par exemple, le module Format fournit l'impression indentée automatiquement. Ce module redéfinit les fonctions d'impression habituelles print_string, print_int, print_float, etc. Quand on utilise Format, on l'ouvre donc systématiquement en début de fichier. Si on ne le faisait pas, on courerait le risque d'oublier la qualification d'une fonction d'impression, ce qui passerait la plupart du temps inaperçu car des fonctions ayant le même nom que celles de Format existent dans l'environnement par défaut (Pervasives). Ce mélange produirait des erreurs dans l'affichage très difficile à retrouver dans le code source. Considérons par exemple
let f () = Format.print_string "Hello World!"; print_newline ();;
Cette fonction est subtilement erronée, car elle n'appelle pas Format.print_newline pour vider la queue du pretty-printer, si bien que "Hello World!" reste en attente dans Format, tandis que Pervasives.print_newline écrit un simple retour charriot sur la sortie standard ... Si Format imprime sur un fichier tandis que la sortie standard est reliée au terminal les choses se corsent pour trouver qu'un retour charriot manque dans le fichier (et que l'impression sur le fichier est bizarre puisque des boîtes qui devaient être fermées par Format.print_newline sont restées ouvertes), tandis qu'un innocent retour-charriot est apparu sur le terminal!
Pour la même raison, on ouvre aussi les grosses bibliothèques comme celle des grands entiers afin d'alléger le programme qui les utilise.
open Num;;
let rec fib n =
if n <= 2 then Int 1 else fib (n - 1) +/ fib (n - 2);;
Justification: Le programme serait moins lisible si l'on devait qualifier tous les identificateurs.
Dans un programme où les définitions de type sont partagées, il est bon de réunir ces définitions dans un ou des module(s) sans implémentation (ne contenant que des types). Il est alors admis d'ouvrir systématiquement le module qui exporte les définitions de type partagées. Filtrages On n'aura jamais peur d'abuser du filtrage! En revanche, on aura soin d'éviter les filtrages non exhaustifs
Ils seront complétés avec soin, sans utiliser une clause ``attrappe-tout comme | _ -> ... ou | x -> ... quand il est possible de s'en passer (par exemple lorsqu'il s'agit de filtrages sur des types concrets définis dans le programme). Voir aussi les les avertissements du compilateur. Avertissements du compilateur
Les avertissements du compilateurs sont destinés à prévenir des erreurs potentielles; c'est pourquoi il faut impérativement en tenir compte et corriger vos programmes si leur compilation produit de tels avertissements. De plus, les programmes dont la compilation produit des avertissements ont un parfum d'amateurisme qui ne sied certainement pas à vos propres oeuvres! Avertissements sur le filtrage
Les avertissements concernant le filtrage doivent être traités avec le plus grand soin:
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 liste l et les deux éléments qu'elle contient x et y. let avec filtre très simple: let _ = ... ne définit rien du tout, se contentant en fait d'évaluer l'expression à droite du signe =.
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: les let destructurant qui donnent un avertissement du compilateur seront systématiquement remplacés par un filtrage explicite. Par exemple, au lieu de let [x; y] as l = List.map succ (l1 @ l2) in expression, on écrira:
match List.map succ (l1 @ l2) with | [x; y] as l -> expression | _ -> assert false
Pour les définitions globales à l'aide de let destructurants, on réécrira la définition avec un filtrage explicite et en utilisant des n-uplets intermédiaires:
let x, y, l = match List.map succ (l1 @ l2) with | [x; y] as l -> x, y, l | _ -> assert false
Justification: Il n'y a pas de moyen de rendre le filtrage exhaustif si l'on utilise des let destructurant généraux.
Avertissements sur la séquence et let _ = ...
Lorsque le compilateur émet un avertissement sur le type d'une expression d'une séquence, on doit explicitement indiquer qu'on désire ignorer le résultat de cette expression. Pour cela,
on utilise une liaison vide, et pour supprimer l'avertissement de:
List.map f l; print_newline ()
on écrit
let _ = List.map f l in print_newline ()
on utilise aussi la fonction prédéfinie ignore : 'a -> unit qui ignore son argument pour rendre unit. On écrit alors
ignore (List.map f l); print_newline ()
mais le meilleur moyen est encore de réfléchir sur son code et de se demander pourquoi l'on obtient cet avertissement. Le compilateur émet une alerte parce que le programme calcule un résultat inutile puisqu'il est jeté. Donc, s'il n'est complètement inutile, ce calcul ne sert que par ses effets; il devrait donc le signaler en retournant la valeur rien. La plupart du temps l'alerte indique qu'on n'a pas appelé la bonne fonction, ou bien que la fonction nécessaire n'existe pas et doit être écrite. Ici, on est dans le premier cas, et il fallait évidemment écrire
List.iter f l; print_newline ()
Dans les programmes, il arrive que la version procédurale (celle qui ne fait que les effets de bord) de la fonction n'existe pas et qu'il faille l'écrire: très souvent une séparation des parties fontionnelle et purement procédurale de la fonction résoud élégamment le problème et le programme gagne en lisibilité! Par exemple, on pourrait réécrire la définition suivante (qui mélange allègrement calculs et effets de bord)
let add x y = if x > 1 then print_int x; print_newline (); x + y;;
en deux définitions séparées:
let print_adder x = if x > 1 then print_int x; print_newline ();;
let add x y = x + y;;
et modifier en conséquence les appels à add.
Attention cependant, la construction let _ = ... ne doit être utilisée que ponctuellement, et pour expliciter un résultat ignoré. Elle ne doit évidemment pas remplacer systématiquement la séquence.
Justification: La séquence est bien plus claire! Comparer e1; e2; e3 à
let _ = e1 in let _ = e2 in e3
Fonctions hd et tl
On n'utilisera pas les fonctions hd et tl mais un filtrage explicite de la liste argument.
Justification: C'est tout aussi bref et bien plus clair que l'usage de hd et tl qui doit nécessairement être protégé par un try... with... pour rattraper l'exception qui peut être déclenchée par ces fonctions.
Boucles Boucles for
Pour parcourir simplement un vecteur ou une chaîne on utilisera une boucle for.
for i = 0 to Array.length v - 1 do ... done
Si la boucle est complexe ou rend un résultat on utilisera une fonction récursive.
let find_index e v =
let rec loop i = if i >= Array.length v then raise Not_found else if v.(i) = e then i else loop (i + 1) in loop 0;;
Justification: La fonction récursive permet de coder simplement n'importe quelle boucle, même complexe, par exemple à sorties multiples ou avec des pas d'indices bizarres (un pas dépendant de la valeur d'une donnée par exemple).
En outre, la boucle récursive évite d'utiliser des mutables dont la valeur peut-être modifiée dans n'importe quelle partie du corps de la boucle (ou même à l'extérieur): au contraire la boucle récursive prend explicitement en argument les variables dont les valeurs sont susceptibles de changer lors des appels récursifs.
Boucles while
Loi des boucles while: Attention: une boucle while est habituellement fausse, à moins que son invariant de boucle n'ait été explicité.
La principale utilisation de la boucle while est la boucle sans fin while true do .... On en sort par une exception, généralement à la terminaison du programme.
Les autres boucles while sont très difficiles d'emploi, à moins qu'elles ne proviennent de programmes tout faits des cours d'algorithmique où elles ont été prouvées.
Justification: Les boucles while nécessitent un ou des mutables pour que la condition de la boucle change de valeur et que la boucle finalement termine. Pour prouver la correction, il faut donc mettre à jour des invariants de boucle, sport intéressant mais difficile.
Exceptions
Ne pas craindre de définir ses propres exceptions dans les programmes, mais à l'inverse utiliser au maximum les exceptions prédéfinies du système. Par exemple toute fonction de recherche qui échoue devrait déclencher l'exception prédéfinie Not_found. Ayez soin de surveiller les exceptions qui peuvent être déclenchées par les appels de fonction à l'aide d'un try ... with.
L'utilisation d'un rattrapage de toutes les exceptions par un try ... with _ -> est normalement réservée à la fonction principale du programme. Si l'on a besoin de rattraper toutes les exceptions pour préserver un invariant d'un algorithme, on aura soin de nommer l'exception et de la relancer, après avoir remis en place l'invariant. Typiquement:
let ic = open_in ... and oc = open_out ... in try
treatment ic oc; close_in ic; close_out oc
with x -> close_in ic; close_out oc; raise x
Justification: try ... with _ -> rattrape silencieusement toutes les exceptions même celles qui n'ont rien à voir avec le calcul en cours (par exemple une interruption sera capturée et le calcul continuera malgré tout!).
Structures de données
L'une des grandes forces de Caml réside dans la puissance des structures de données définissables et la simplicité de leur manipulation. Il faut donc en profiter au maximum et ne pas hésiter à définir ses propres structures de données. En particulier, il ne faut pas représenter systématiquement des énumérations par des entiers, ni des énumérations à deux cas par des booléens. Exemples:
type figure =
| Triangle | Square | Circle | Parallelogram;;
type convexity =
| Convex | Concave | Other;;
type type_of_definition =
| Recursive | Non_recursive;;
Justification: Une valeur booléenne masque souvent l'intuition du codage correspondant. Par exemple, si type_de_définition est codé par un booléen, que signifie true ? Définition «normale» (c'est-à-dire non récursive) ou définition récursive ?
Dans le cas d'un type énuméré codé par un entier, il est très difficile de limiter l'intervalle des entiers acceptables: il faut définir des fonctions de construction qui assurent les invariants du programme (et s'assurer ensuite qu'on ne construit jamais de valeurs autrement qu'en appelant ces fonctions de construction), ou bien truffer le programme d'assertions ou encore de gardes dans les filtrages. Ces problèmes sont élégamment résolus par la définition d'un type somme énuméré, qui permet ensuite d'utiliser à plein le filtrage en profitant du test d'exhaustivité du filtrage que vous offre le compilateur.
Critique: Dans le cas des énumérations binaires, on peut systématiquement définir des prédicats dont le nom porte la sémantique des booléens qui implémentent le type. Supposons aussi que l'on adopte la convention d'indiquer qu'un nom représente un prédicat lorsque ce nom se termine par la lettre p (comme en emacs lisp). Alors, au lieu de définir un type somme pour type_de_définition, on définirait simplement un prédicat recursivep qui rend vrai si la définition est récursive.
Réponse: Cette méthode ne peut être étendue aux énumérations autres que binaires; de plus, elle se marie moins naturellement avec le filtrage. Prenons le cas des définitions récursives ou non, dont la nature serait codée directement par un booléen, provenant donc du cas | Let of bool * string * expression d'un type définition; un cas de filtrage typique sur une définition aura l'allure suivante:
| Let (_, v, e) as def -> if recursivep def then ``code for recursive case else ``code for non recursive case
on encore, si le prédicat recursivep s'applique directement aux booléens:
| Let (b, v, e) -> if recursivep def then``code for recursive case else ``code for non recursive case
En revanche, avec un type explicite on écrirait plutôt:
| Let (Recursive, v, e) -> ``code for recursive case | Let (Non_recursive, v, e) -> ``code for non recursive case
La différence est faible il est vrai, et l'on peut préférer l'un ou l'autre style selon ses goûts. Remarquez cependant que la méthode avec type binaire explicitement défini n'empêche nullement de définir un prédicat pour se ramener au cas utilisant directement des booléens. Note finale: dans le cas de l'utilisation directe des booléens, la définition d'un prédicat n'empêche pas un autre programmeur d'utiliser directement les booléens (en écrivant par exemple | Let (b, v, e) -> if b then ... ce qui est bien typé et tombe dans le travers de sémantique peu claire dénoncé plus haut, ce que le langage interdit lorsqu'on définit un type somme énuméré).
A contrario, il ne faut pas systématiquement définir des drapeaux booléens à l'aide d'un nouveau type, lorsque l'interprétation des constructeurs true et false paraît claire. On peut ainsi discuter de l'utilité des types suivants:
type switch = | On | Off;; type bit = | One | Zero;;
La même objection est recevable pour les types énumérés représentés par des entiers, lorsque les entiers ont une interprétation évidente pour les données à représenter. Utilisation des mutables
Les valeurs mutables sont utiles et quelquefois indispensables à une programmation simple et claire. Il faut cependant les utiliser avec discernement: les structures de données normales de Caml sont non mutables. On privilégie les structures de données non mutables pour la clarté et la sûreté de programmation qu'elles autorisent. Itérateurs
Les itérateurs de Caml sont un trait puissant et utile. Il ne faut cependant pas en abuser, ni a contrario les négliger: il vous sont fournis par les bibliothèques et ont toutes les chances d'être corrects et mûrement réfléchis par l'auteur de la bibliothèque. Il est donc inutile de les réinventer.
On écrira donc
let carre_elements elements = map carre elements;;
plutôt que:
let rec square_elements = function
| [] -> [] | elem :: elements -> square elem :: square_elements elements;;
En revanche, on évitera d'écrire:
let iter f x l =
List.fold_right (List.fold_left f) [List.map x l] l;;
Quoiqu'on obtienne:
val iterator : ('a list -> 'a -> 'a list) -> ('a -> 'a) -> 'a list -> 'a list =
<fun>
- iterator (fun l x -> x :: l) (fun l -> List.rev l) 1; 2; 3 ;;
- : int list list = [[1; 2; 3]; [3; 2; 1]]
En cas de besoin exprès, on aura soin d'ajouter un commentaire explicatif: à mon avis il s'impose! Optimisation des programmes
Pseudo loi de l'optimisation: Pas d'optimisation a priori. Pas d'optimisation a posteriori non plus.
Programmer simple et clair avant tout. Ne commencer l'optimisation que lorsque le goulet d'étranglement du programme a été identifié (en général quelques routines). Alors l'optimisation consiste avant tout à changer la complexité de l'algorithme utilisé. Cela passe souvent par la redéfinition des structures de données manipulées et la réécriture complète de la partie du programme qui pose problème.
Justification: Clarté et correction des programmes sont prioritaires. De plus, dans un programme conséquent, il est pratiquement impossible d'identifier a priori les parties du programme dont l'efficacité est primordiale.
Choisir entre classes et modules
Les classes d'OCaml s'imposent lorsqu'on a besoin d'héritage, c'est-à-dire d'un raffinement incrémental des données et de leurs fonctionnalités.
Les types de données conventionnels (types somme en particulier) s'imposent lorsqu'on a besoin de filtrage.
Les modules s'imposent lorsque les structures de données sont fixes et que leur fonctionnalité est également fixée ou que l'ajout de nouvelles fonctions dans les programmes qui les utilisent est suffisant. Clarté du Code Caml
Le langage Caml comporte des constructions puissantes qui autorisent une programmation simple et claire. Le principal problème pour obtenir un programme limpide est de les utiliser à bon escient...
Le langage propose de nombreux styles de programmation (ou paradigmes de programmation): la programmation impérative, la programmation fonctionnelle, la programmation orientée objet. Le premier travail du programmeur est donc d'employer le paradigme de programmation adapté au problème. Au sein d'un paradigme de programmation, la difficulté est d'employer la construction du langage qui exprime le plus simplement et le plus naturellement le calcul à effectuer. Écueils dans le style
En ce qui concerne les styles de programmation, on observe principalement les deux écueils symétriques du «tout impératif» (on utilise systématiquement des boucles et des affectations) et du «tout fonctionnel» (on n'utilise jamais de boucles ni d'affectation); le style «tout objet» apparaîtra bientôt, n'en doutons pas, mais il est encore trop nouveau pour être décrit ici. Par exemple:
Écueil du «Trop impératif»: On aurait tort d'utiliser un style impératif pour coder une fonction naturellement récursive. Par exemple, pour calculer la longueur d'une liste, on ne devrait pas écrire:
let list_length l = let l = ref l in let res = ref 0 in while !l <> [] do incr res; l := List.tl !l done; !res;;
au lieu de la fonction récursive toute simple:
let rec list_length = function | [] -> 0 | _ :: l -> 1 + list_length l;;
(pour ceux qui contesteraient l'équivalence des deux programmes, voir la note suivante). Un autre écueil du trop impératif est souvent de ne pas utiliser systématiquement la simple boucle for pour parcourir un vecteur, mais au contraire d'utiliser une boucle while complexe, avec une ou des références (trop d'affectations inutiles et trop d'occasions d'erreur). Ce programmeur considère que le mot-clé mutable dans les définitions de types enregistrement devrait être implicite. Écueil du «Trop fonctionnel»: Le programmeur frappé de ce syndrome évite d'utiliser les vecteurs et l'affectation. Dans les cas les plus graves on assiste à un refus complet d'utiliser les aspects impératifs du langage, même quand ils sont de loin les plus naturels. Des symptômes caractéristiques: réécriture systématique des boucles for à l'aide d'une fonction récursive, utilisation de listes dans des contextes où des structures de données impératives s'imposent, passage de nombreux paramètres globaux du problème à toutes les fonctions, même quand une référence globale permettrait d'omettre ces paramètres pour la plupart invariants. Ce programmeur considère que le mot-clé mutable dans les définitions de types enregistrement devrait être supprimé de Caml.
Code Caml habituellement considéré comme illisible
La puissance des constructions disponibles en Caml permet d'écrire du code limpide et extrêmement concis. Cependant cette puissance des constructions autorise aussi l'écriture de code inutilement complexe, jusqu'à obtenir un programme parfaitement illisible.
Voici un certains nombre d'écueils connus:
Classique, l'introduction de conditionnelles if then else absolument inutiles, comme
let flush_ps () = if not !psused then psused := true;;
ou (plus subtilement)
let sync b = if !last_is_dvi <> b then begin last_is_dvi := b end;;
Plus déroutant, le codage d'une construction par une autre. Par exemple, coder un let ... in par l'application d'une fonction anonyme à un argument. On écrirait
(fun x y -> x + y) e1 e2
au lieu d'écrire simplement
let x = e1 and y = e2 in x + y
Dans le même esprit, le codage systématique de la séquence avec des let in est assez ``populaire. Encore plus méchant, le mélange des calculs et des effets, particulièrement dans les appels de fonctions. Rappelons que l'ordre dans lequel sont évalués les arguments d'une application n'est pas spécifié, ce qui implique qu'on ne doit pas mélanger effets et calculs dans les appels de fonctions. Cependant, lorsqu'il n'y a qu'un argument à la fonction, on peut en profiter pour faire un effet dans l'argument, ce qui est extrêmement troublant pour le lecteur bien que sans danger pour la sémantique du programme. À proscrire absolument. Plus spécifique aux langages de haut niveau, l'utilisation abusive (c'est à dire excessive, ou mal à propos, ou encore notoirement insuffisante) des itérateurs et des fonctions d'ordre supérieur. Par exemple, il vaut mieux utiliser List.map ou List.iter que d'écrire en ligne leurs équivalents en termes de fonction récursive. Pire encore, ne pas utiliser List.map ou List.iter mais leur équivalent en ligne, exprimé avec List.fold_right ou List.fold_left. La reine des méthodes pour obtenir un code illisible est évidemment de mélanger toutes ou partie des méthodes précédentes. Par exemple:
(fun u -> print_string "world"; print_string u) (let temp = print_string "Hello"; "!" in ((fun x -> print_string x; flush stdout) " "; temp));;
Si vous écrivez naturellement ainsi le programme print_string "Hello world!", vous pouvez sans doute soumettre vos oeuvres au concours du code Caml le plus illisible. 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 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. 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 termes 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.