OCaml pour le programmeur : Un aperçu

January 14, 2008 § 3 Comments

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.

Avec un peu d’expérience d’OCaml, vous constaterez qu’OCaml est un langage dans lequel les programmes écrits sont beaucoup plus concis qu’en Java ou C#, grâce à de puissantes primitives d’abstraction, grâce à l’extensibilité du langage, grâce à l’inférence de types et, tout simplement, grâce à une syntaxe plus concise. Cette concision est souvent un avantage, même si elle rend le code plus dense et donc parfois plus difficile à lire.

Pourquoi apprendre OCaml ?

Cette réponse a plusieurs questions.
Pour les mathématicien, physiciens, statisticiens ou autres utilisateurs de programmes scientifiques, OCaml permet de développer des programmes rapides et fiables, d’une part parce que le code source du programme ressemblera à la description mathématique du problème et sera donc plus simple à vérifier et d’autre part parce que les analyses permises par OCaml éviteront de nombreuses erreurs. De plus, la programmation fonctionnelle de haut niveau simplifie la constitution des bibliothèques génériques et aisément réutilisables.
Les programmeurs Java, C#, C, C++ ou autres utilisateurs de langages ou librairies système ou orientés-objets découvriront avec OCaml une autre manière de voir le monde et d’écrire des programmes dépendant moins du mode d’exécution de la machine et plus des résultats voulus, de modifier le langage de programmation pour l’adapter au problème au fur et à mesure de la progression du projet, ou encore d’autres techniques de factorisation et de réutilisation de code. Même si vous ne deviez pas utiliser OCaml dans votre carrière, les concepts vous permettront d’analyser un grand nombre de problème sous des angles que vous n’aviez pas prévus jusqu’à présent.
Les programmeurs Python, Ruby, JavaScript découvriront un langage confortable, dans lesquels ils retrouveront un grand nombre d’idées connues, sous une forme plus rigoureuse et plus adaptée au développement de grands projets critiques.

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
Cette définition mathématique se traduit naturellement en OCaml par

value delta = fun
[ 0.0 -> 1.0
| _ -> 0.0 ];

ou encore

value delta x = match x with
[ 0.0 -> 1.0
| _ - > 0.0];

ou encore

value delta x = if x=0.0 then 1.0 else 0.0;

Ces trois extraits se lisent “delta est la fonction qui à 0,0 associe 1,0 et qui à toute autre valeur associe 0,0″. Retenez la structure des deux premiers extraits, qui reviendra fréquemment. Profitons-en pour préciser que le test d’égalité se note “=”, qu’il fonctionne correctement, contrairement à celui de Java ou C# (dans lesquels il faut parfois utiliser “=” et parfois invoquer une méthode) ou Python (qui, par défaut, vérifie l’identité et non l’égalité).

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
Ces deux définitions peuvent être utilisées en OCaml.

Définition récursive

Ainsi, on peut traduire directement la deuxième définition par

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

Un dernier exemple, bien plus complexe, de manipulation de fichiers, à l’aide de quelques bibliothèques.


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é.
La deuxième partie de l’extrait fait appel à des fonctions définies dans une bibliothèque :
  • 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).
Cet extrait se lit donc Ouvrons pour lecture le fichier “/tmp/in.txt” en nous assurant qu’il sera refermé à la fin de notre tâche et appelons son contenu input. Ouvrons pour écriture le fichier “/tmp/out.txt” en nous assurant qu’il sera refermé à la fin de notre tâche et appelons son contenu output. Lisons input ligne par ligne, appliquons le filtre add_line_number pour numéroter ces lignes, en partant du numéro 1, et écrivons le résultat de tout ceci dans output.
En Java, pour obtenir un résultat similaire, on aurait ajouté au programme quelque chose comme

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();
}
//...

La complexité des deux programmes est comparable, même si l’on peut constater que, de nouveau, les bibliothèques de OCaml sont de plus haut niveau et donc plus simples à composer sans commettre d’erreurs. En particulier, dans la version Java, de nombreuses erreurs peuvent se glisser, qui ne seraient pas détectées par le compilateur, par exemple oublier de fermer les flux, oublier d’avancer progressivement dans un flux, tester si la ligne est vide au lieu de déterminer si elle vaut null, etc.. À l’inverse, dans la version OCaml, le programmeur ne peut commettre aucune de ces erreurs.
Si nécessaire, OCaml fournit aussi les bibliothèques nécessaires pour gérer les fichiers à peu près de la même manière qu’en Java, ou pour traiter les expressions régulières.

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.

  1. Bases d’OCaml : ligne de commande, syntaxe, types de base, valeurs, fonctions, inférence de types, polymorphisme paramétrique.
  2. Structures de données : listes, tableaux, enregistrements, types sommes, types polymorphes, filtrage par motifs.
  3. Compilation : environnements, compilation, compilation native, modules, ocamlbuild.
About these ads

Tagged: , , , , , , , ,

§ 3 Responses to OCaml pour le programmeur : Un aperçu

  • Simon Petitjean says:

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

    Merci pour les encouragements.
    Comment se passe la troisième année ?

  • Simon Petitjean says:

    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…

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

What’s this?

You are currently reading OCaml pour le programmeur : Un aperçu at Il y a du thé renversé au bord de la table.

meta

Follow

Get every new post delivered to your Inbox.

Join 29 other followers

%d bloggers like this: