01.14.08
OCaml pour le programmeur : Un aperçu
Update: Depuis la rédaction de ce billet, j’ai décidé de faire de cette introduction un WikiLivre. La dernière version de cet aperçu se trouve donc sur le site de WikiLivres.
Ce billet signe le début d’une introduction au langage OCaml, prévue pour les programmeurs ayant déjà une expérience d’un autre langage de programmation. Il ne s’agit donc pas d’apprendre à programmer mais d’apprendre les spécificités du langage OCaml — et comment s’en servir au mieux.
Qu’est-ce qu’OCaml ?
OCaml est un langage de programmation, c’est-à-dire une manière de donner des instructions à l’ordinateur pour obtenir des résultats ou des effets.
Tout comme Java, C# ou Python, OCaml est un langage de haut niveau conçu pour écrire des applications et des bibliothèques de programmation évoluées, sans avoir à se préoccuper de questions de bas niveau, telles que la gestion de la mémoire, et conçu pour favoriser la réutilisation de code ou de composants. Tout comme ces langages, OCaml dispose de nombreuses bibliothèques spécialisées qui permettent de manipuler des interfaces graphiques, des images 3D, de services web, des sons et musiques…
Par opposition à Java, C# ou Python, qui sont des langages impératifs OCaml entre dans la catégorie des langages fonctionnels. Là où un langage impératif décrira un comportement sous la forme d’une suite de modifications de la mémoire vive et de l’état de l’ordinateur, un langage fonctionnel décrira un comportement sous la forme d’un ensemble de fonctions mathématiques, qui permettent de déduire un résultat à partir d’entrées. OCaml est aussi un langage multi-paradigme, c’est-à-dire que, lorsque la programmation fonctionnelle ne suffit pas, il reste possible d’utiliser la programmation impérative ou/et la programmation orientée-objets, c’est-à-dire à se ramener à une programmation qui ressemblera à Java.
Les concepteurs de Java ou C# affirment que ce langage est statiquement et fortement typé, ce qui n’est que partiellement vrai. Par opposition, OCaml est effectivement statiquement fortement typé, ce qui signifie que le langage de programmation procède automatiquement à de nombreuses vérifications et rejette les programmes considérés comme erronés ou programmés avec insuffisamment de rigueur. De plus, le typage d’OCaml se fait par inférence, ce qui signifie que l’essentiel des vérifications se fait de manière transparente, sans avoir à donner d’informations supplémentaire à OCaml, contrairement à ce qui se passe en Java ou en C# dans lequel il faut préciser le type de chaque variable, de chaque argument, chaque méthode… Là où les langages dynamiquement typés tels que Python ne contrôlent la cohérence des opérations que durant l’exécution du programme, OCaml analyse celle-ci intégralement lors de la phase de compilation, ce qui siplifie considérablement la phase de tests.
Par opposition à Java, C# ou Python, OCaml essaye le plus possible d’être un langage déclaratif, c’est-à-dire un langage dans lequel on va décrire la solution à un problème, plutôt que la construire étape par étape. Pour ce faire, OCaml est extensible, ce qui signifie que, lorsqu’une application a besoin d’une opération complexe et répétitive, il est possible d’ajouter cette opération sous la forme d’une nouvelle primitive du langage lui-même — ou même d’ajouter à l’intérieur d’OCaml un langage de programmation spécialisé dans la résolution de votre problème. Ainsi, certaines bibliothèques, au lieu d’ajouter de nouvelles fonctionnalités au langage, modifient ce dernier.
Par opposition à Java, C# ou Python, OCaml essaye le plus possible d’être un langage de haute performance. Ainsi, un programme OCaml démarre beaucoup plus vite, s’exécutera fréquemment plus vite et consommera fréquemment 4 fois moins de mémoire vive qu’un programme Java ou C#. Un programme OCaml utilisera généralement un peu plus de mémoire vive qu’un programme Python mais s’exécutera souvent 10 fois plus vite. Dans certains cas, heureusement assez rares, ce gain de performances se fait au détriment du confort de programmation.
Ah, encore une chose : par opposition à Java ou C#, et comme Python, OCaml est et reste un langage d’expérimentation, ce qui signifie qu’il gagne régulièrement de nouvelles fonctionnalités inédites. Il existe ainsi des versions d’OCaml spécialisées dans la programmation distribuée (JoCaml, Acute), dans la manipulation d’arbres XML (OCamlDuce), dans l’écriture de compilateurs (MetaOCaml) etc.
Pourquoi apprendre OCaml ?
Avant d’enseigner
Avant d’entrer dans l’enseignement, commençons par une mise en bouche.
Note: Tout ce qui suit utilise la syntaxe révisée de OCaml, qui est l’une des manières possibles d’écrire du OCaml, et qui est incluse dans OCaml sous la forme de la librairie camlp4r.
Bonjour, le monde
L’extrait suivant est un programme complet d’une ligne, qui a pour effet d’afficher le texte “Bonjour, le monde” :
print_endline (”Bonjour, le monde.”);
On pourrait d’ailleurs simplifier légèrement le programme en supprimant les parenthèses, qui ne sont ici pas nécessaires, de manière à obtenir l’extrait suivant :
print_endline "Bonjour, le monde.";
Dans ce qui précède, print_endline est une fonction, dont l’effet est d’afficher du texte et de passer à la ligne, et qui n’a pas de résultat. En Java, ce serait l’équivalent de
public class Main
{
public static final void main(String args[])
{
System.out.println(”Bonjour, le monde”);
}
}
Pi
Pour définir la valeur de pi, il suffit d’écrire
value pi = 3.1415926;
Cette ligne donne le nom pi à la valeur 3.1415926. Cette valeur ne changera plus au cours de l’exécution du programme. En Java, ce serait l’équivalent d’une déclaration
public static final float PI = 3.1415926;
Identité
Les fonctions sont des valeurs comme les autres. Ainsi, pour définir la fonction identité, qui à tout x associe x lui-même, on peut écrire
value id = function x -> x;
ou encore
value id = fun x -> x;
ou encore
value id(x) = x;
ou encore
value id x = x;
Ces quatre définitions sont équivalentes. Les deux premières précisent explicitement que id est une fonction, tandis que les deux dernières peuvent se lire “pour tout x, id(x) vaut x” — dans la dernière version, nous avons tout simplement supprimé les parenthèses qui étaient inutiles. Nous verrons plus tard que cette fonction va faire l’objet d’un certain nombre de vérifications à chaque fois qu’elle est appliquée.
On utilise alors id de la manière suivante
value y = id 5;
value z = id “Il fait beau”;
En Java, pour obtenir la même fonctionnalité, le plus simple est d’abandonner toute vérification et d’écrire une classe contenant la méthode suivante :
public class Fonctions
{
public static Object id(Object x)
{
return x;
}
}
On utilise alors id de la manière suivante
int y = (Integer)Fonctions.id(5);
String z = (String)Fonctions.id(”Il fait beau”);
Si l’on cherche à conserver une vérification de types, il devient nécessaire de définir une classe plus complexe :
public class Id<T>
{
public T appliquer(T x)
{
return x;
}
}
On utilise alors id de la manière suivante
int y = new Id<Integer>().appliquer(5);
String z = new Id<String>(). appliquer(”Il fait beau”);
Profitons de cet exemple pour remarquer que, en OCaml, le mot-clé return n’existe pas et n’est pas nécessaire. En effet, le corps d’une fonction est une expression et toute expression est évaluée. En d’autres termes, le résultat d’une fonction est donné par la dernière expression de cette fonction.
Delta
La fonction delta de Dirac est définie mathématiquement de l’ensemble des réels vers l’ensemble des réels par
- delta(0) = 1
- pour tout autre x, delta(x) = 0
value delta = fun
[ 0.0 -> 1.0
| _ -> 0.0 ];
value delta x = match x with
[ 0.0 -> 1.0
| _ - > 0.0];
value delta x = if x=0.0 then 1.0 else 0.0;
Opérateurs logiques
OCaml définit bien entendu les opérateurs logiques habituels. On peut aussi, si nécessaire, les redéfinir à la main. Ainsi, le ou exclusif logique peut s’écrire
value ou_exclusif a b = match (a,b) with
[ (True, True) -> False
| (True, False) -> True
| (False, True) -> True
| (False, False) -> False ];
Ce qui peut se résumer
value ou_exclusif a b = match (a,b) with
[ (True, False) | (False, True) -> True
| _ -> False ];
Dans ce qui précède, True et False sont les noms donnés en OCaml aux booléens vrai et faux. L’utilisation de match (a,b) permet de prendre des décisions en fonction de la structure du couple (a,b).
Bien entendu, la même fonctionnalité pourrait s’écrire, de manière moins lisible, à l’aide des opérateurs logiques habituels :
value ou_exclusif a b = (a && not b) || (b && not a);
En Java, les premiers extraits se traduiraient
public static bool ou_exclusif(bool a, bool b) {
if ((a==true && b == false) || (a==false && b== true))
return true;
else
return false;
}
Le dernier extrait s’écrirait
public static bool ou_exclusif(bool a, bool b)
{
return (a && !b ) || (!a && b );
}
Factorielle
La fonction mathématique factorielle peut se définir de plusieurs manières. Directement, avec des points de suspension, on écrira factorielle(n) = 1 * 2 * … * n. Ou encore sans points de suspension, par récurrence, à l’aide de
- pour tout n >= 1, factorielle(n) = n * factorielle (n - 1) .
- pour tout autre n, factorielle(n) = 1
Définition récursive
value rec factorielle = fun
[ n when n>=1 -> n * (factorielle (n - 1) )
| _ -> 1 ];
Cet extrait se lit exactement comme la définition par récurrence.
Dans ce qui précède, rec signifie que la fonction est définie par récursivité, mécanisme qui correspond à la définition mathématique par récurrence ou par induction : les cas non-triviaux sont déterminés à partir des cas précédents, ou encore la fonction s’appelle elle-même. En OCaml, la récursivité est la forme de boucle la plus fréquente.
On pourrait aussi écrire le code suivant, toujours récursif :
value rec factorielle n = if n >= 1 then n * ( factorielle (n - 1) ) else 1;
En Java, on recommande d’écrire :
public static int factorielle(int n)
{
int result = 1;
for(int i = 1; i < n; ++i)
result *= i;
return result;
}
Il est aussi possible d’écrire une version récursive en Java, même si, à cause des limites de ce langage, ce genre de pratiques est généralement évité :
public static int factorielle(int n)
{
if(n<=0)
return 1;
else
return n * factorielle (n-1);
}
Comme on peut le constater, les versions OCaml sont plus concises et plus proches de la définition mathématique que la version Java recommandée. Même si cela n’est pas fondamental pour un exemple aussi simple, on peut déjà constater qu’il y a un peu moins d’occasions de commettre des erreurs simples en suivant la structure OCaml que la structure Java recommandée. Cette tendance ne fait que s’amplifier pour des exemples plus complexes.
Définition directe
Sans entrer dans les détails pour le moment, on peut définir le produit de tous les éléments d’une liste comme
value produit = List.fold_left ( * ) 1;
Cette ligne se lit “le produit de a1, a2, …, an est 1*a1*…*an”. Le terme 1 est nécessaire pour que cette définition du produit généralisé fonctionne même dans le cas où n=0.
Le produit des entiers de 1 à n s’écrit alors
value factorielle n = produit [ 1 .. n ];
Note: Cette notation [ 1 .. n ] est définie dans une extension de OCaml.
Manipulation de fichiers
open MoreStream;
open MoreFiles;
value rec add_line_number i stream = match stream with parser
[ [: `line; rest :] -> [: `((string_of_int i)^": "^line); add_line_number (i+1) rest :]
| [: :] -> [: :] ];
with_input “/tmp/in.txt” (function input ->
with_output “/tmp/out.txt”(function output ->
output_lines (add_line_number 1 (input_lines input)) output
)
);
La première partie de l’extrait définit un filtre sur les flux (streams) nommé add_line_number. Ce filtre est conçu pour traiter flux de lignes, et son rôle est d’ajouter au début de chaque ligne le numéro de cette ligne. Les symboles [:, :] et ` sont utilisés pour examiner le contenu d’un flux ou construire de nouveaux flux. Ainsi, [: :] désigne le flux vide et [: `line; rest :] désigne n’importe quel flux (d’entrée) non vide, dans lequel on choisit d’appeler line le premier élément et rest la suite. De même, [: `((string_of_int i)^": "^line); ... désigne le flux (de sortie) dans lequel le premier élément s'obtient en convertissant l'entier i en chaîne de caractère, chaîne à laquelle on concatène le symbole ": " suivi de line (qui représente toujours le premier élément du flux d'entrée). Enfin, ... ; add_line_number (i+1) rest :] désigne la suite du flux de sortie, qui s’obtient en continuant la boucle add_line_number, dans laquelle le nombre i est remplacé par i+1 et dans laquelle on s’intéresse à flux rest (qui représente toujours la suite du flux d’entrée).
La boucle add_line_number se lit donc
- tant que le flux d’entrée n’est pas épuisé, prendre le prochain élément (appelons-le line) et utilisons-le pour construire le prochain élément du flux de sortie — cet élément aura pour valeur la représentation du nombre i (le numbre de lignes rencontrées depuis le début de la boucle), suivi de deux points, suivi de line
- une fois le flux d’entrée épuisé, le flux de sortie est terminé.
- with_input ouvre un fichier en lecture, exécute une tâche sur ce fichier puis le referme, y compris en cas d’erreur
- with_output ouvre un fichier en écriture, exécute une tâche sur ce fichier puis le referme, y compris en cas d’erreur
- output_lines écrit le contenu d’un flux de lignes dans un fichier (ici, il s’agit du flux de sortie construit plus haut)
- input_lines lit un fichier comme une suite de lignes, sous la forme d’un flux (ici, il s’agit du flux d’entrée consulté plus haut).
import java.io.*;
//…LineNumberReader reader = new LineNumberReader(new FileReader(”/tmp/in.txt”));
try
{
try
{
PrintWriter writer = new PrintWriter(new FileWriter(”/tmp/out.txt”));
reader.setLineNumber(1);
for(String line = reader.readLine(); line != null; line = reader.readLine())
writer.print(reader.getLineNumber+”: “+line)
} finally {
writer.close();
}
} finally {
reader.close();
}
//…
null, etc.. À l’inverse, dans la version OCaml, le programmeur ne peut commettre aucune de ces erreurs.Programme des hostilités
Si ces quelques échantillons vous ont intéressé, les prochains billets vous enseigneront comment écrire un programme par vous-même et surtout comment tirer avantage des spécificités d’OCaml.
- Bases d’OCaml : ligne de commande, syntaxe, types de base, valeurs, fonctions, inférence de types, polymorphisme paramétrique.
- Structures de données : listes, tableaux, enregistrements, types sommes, types polymorphes, filtrage par motifs.
- Compilation : environnements, compilation, compilation native, modules, ocamlbuild.
- …
Simon Petitjean a dit,
janvier 23, 2008 à 6:20
Bonne initiative de votre part, Ocaml, c’est bien, et c’est la principale raison pour laquelle il faut l’apprendre. Merci de nous l’avoir fait découvrir avec autant d’enthousiasme
yoric a dit,
janvier 26, 2008 à 4:51
Merci pour les encouragements.
Comment se passe la troisième année ?
Simon Petitjean a dit,
janvier 27, 2008 à 7:42
Bien bien pour le moment, pas de langage aussi motivant, et des matières carrément pas motivantes (genre systèmes d’information :/ )
Sinon le deuxième semestre promet d’être passionnant avec l’excellent ADA pour remplacer Ocaml, ça promet d’être moins marrant…