Introduction à OCaml

OCaml (anciennement Objective Caml) est un langage de script et de programmation. Ce document à pour but d'en proposer une introduction simple, concise et d'être accessible à tous, et ce, même sans connaissances préalable en programmation ou scripting.
Si vous êtes débutant et que vous ne comprennez pas certaines parties, vous pouvez poser des questions sur discuss.ocaml.org.
Tout d'abord une courte présentation d'OCaml. Il s'agit d'un projet initié par l'INRIA (inria.fr, wp).
Le langage OCaml fournit une bonne sureté d'exécution notament grâce à son système de typage de variables, qui permet par exemple d'éviter de passer en paramètre à une fonction une chaîne de charactère là où il faut passer un nombre.


Les différents modes d'exécution

OCaml fournit plusieurs modes d'exécution possibles :
Tous ces modes d'exécution seront vus plus en détail ultérieurement. Dans ce premier épisode nous n'utiliserons que la boucle interactive.

Les styles de programmation

OCaml offre de plus la possibilité d'adopter la plupart des styles de programmation : fonctionnel, impératif, objet, modulaire et de les mélanger. Malgré tout OCaml se prête particulièrement bien au style de programmation fonctionnel, car utilisé correctement, les effets de bord étant alors évités, et associé à la catégorisation des variables, la sureté d'exécution est alors excellente, et il est virtuellement possible de réaliser des logiciels garantis sans aucuns bugs.
Voici pour vous mettre l'eau à la bouche et vous donner l'envie de continuer. Maintenant pour pouvoir suivre le premier volet de cette initiation, téléchargez et installez OCaml à partir de cette adresse https://ocaml.org/docs/install.fr.html ou bien à l'aide de l'installeur fournit par votre distribution.
Ou encore à partir des sources qui sont disponibles par exemple ici :
https://caml.inria.fr/pub/distrib/

Lancez l'interpréteur intractif

Maintenant la première chose à faire est de commencer à se familiariser avec le système de typage d'OCaml. Rassurez-vous, même si vous trouverez peut-être cela contraignant au début, on intègre très vite ce système pour l'utiliser de manière naturelle.
Pour commencer à s'y frotter, le meilleur moyen est d'utiliser l'interpréteur interactif (le "toplevel"). Pour cela commencez une nouvelle session en tapant dans votre console la commande 'ocaml'. La version d'OCaml sera alors affichée en guise d'introduction, suivie du prompt d'OCaml qui attendra que vous entriez la première expression :
[guest@localhost ˜]$ ocaml
        OCaml version 4.11.1

#
Toutes les expressions ou suite d'expressions doivent se terminer par deux points virgule comme ceci ';;'.
Pour comprendre le système de typage nous allons tout d'abord voir les types primitifs qui sont les éléments de base.
Vous pouvez également améliorer les capacités d'édition de l'interpréteur interactif en utilisant rlwrap (que vous devez également installer si vous voulez l'utiliser) :
[guest@localhost ˜]$ rlwrap ocaml
        OCaml version 4.11.1

#

Les types primitifs

1) Le type entier (décimal), tapez au prompt :
# 6 ;;
OCaml vous indiquera alors en réponse le type de l'expression que vous venez d'entrer, suivit de sa valeur :
- : int = 6
2) Le type nombre à virgule (flottante) :
# 8.2 ;;
- : float = 8.2
Le point est obligatoire pour indiquer le type flottant, même s'il n'y a rien après :
# 12. ;;
- : float = 12.
Sinon, le nombre serait reconnu comme un entier :
# 12 ;;
- : int = 12
3) Le type booléen, dont la valeur vaut soit vraie soit fausse :
# true ;;
- : bool = true
# false ;;
- : bool = false
4) Le type caractère (un et un seul caractère). Ils sont indiqués entre guillemets simple :
# 'e' ;;
- : char = 'e'
5) Le type chaîne de caractères (zéro ou plusieurs caractères), placées entre guillemets doubles :
# "OCaml" ;;
- : string = "OCaml"
L'opérateur de concaténation des chaînes de caractère est :
# "OC" ^ "aml";;
- : string = "OCaml"
Pour accéder à un des caractères d'une chaîne de caractères, vous pouvez procéder comme suit :
# "OCaml".[2] ;;
- : char = 'a'
Comme vous pouvez le constater le type renvoyé est bien le type caractère char vu précédemment. L'index commence à zéro pour le premier caractère.
Si on tente d'accéder à un caractère au delà du dernier, une exception est levée :
# "OCaml".[26] ;;
Exception: Invalid_argument "index out of bounds".
Définir, lever et rattraper des exceptions sera vu ultérieurement.
6) Le type « unit » :
# () ;;
- : unit = ()
Ce type est une sorte d'entité vide, avec une valeur unique. Il est utilisé par exemple lorsqu'une fonction ne prend pas d'argument particulier. C'est également le type renvoyé par une fonction qui réalise une action mais ne renvoie aucune valeur particulière. Ce type n'est pas à proprement parlé l'équivalent de ce que l'on retrouve sous le nom de « void » ou « NULL » dans d'autres langages, car l'usage du type unit peut avoir d'autres usages.
Une fonction n'ayant pas une réelle valeur de retour retourne le type « unit » :
# print_endline "OCaml" ;;
OCaml
- : unit = ()
Ici la fonction réalise une action (afficher du texte) et ne retourne rien, à part le type « unit ».
# print_newline () ;;

- : unit = ()
La fonction print_newline affiche simplement une nouvelle ligne, et ne prend pas réellement de paramètre, donc on lui passe simplement le type unit pour l'exécuter.

Transtypage

Certains langages de programmation convertissent les variables implicitement, en OCaml, il faut le faire explicitement.
(Vous rencontrerez parfois aussi le terme "coercition" à la place de transtypage.)
Pour obtenir un nombre à virgule à partir d'un entier :
# float_of_int 16 ;;
- : float = 16.
Et inversement :
# int_of_float 12.0 ;;
- : int = 12
Le fait de ne pas convertir implicitement les entiers et les flottants permet d'éviter certaines erreurs de programmation.
Pour obtenir le code ascii (encodage de caractères) d'un caractère :
# int_of_char 'A' ;;
- : int = 65
Et inversement :
# char_of_int 97 ;;
- : char = 'a'
Idem pour réaliser des opérations arithmétiques, les nombres doivent être de même type :
# 4 + 6 ;;
- : int = 10
Les opérateurs arithmétiques agissant sur les flottants doivent être suffixés par un point :
# 2.4 *. 1.2 ;;
- : float = 2.88
Combinaison des deux types avec transtypage :
# (float) 9 /. 1.2 ;;
- : float = 7.5
Pour réaliser un transtypage il ne s'agit pas d'indiquer le type souhaité avant l'expression entre parenthèse comme pourrait le suggérer cet exemple, il faut utiliser une fonction qui réalise la conversion de type. En l'occurrence ici float n'est qu'un alias de la fonction float_of_int vue plus haut, et placer cette fonction dans un tuple (les tuples seront vus plus bas) n'influe en rien sur son fonctionnement.

Définir une valeur

Pour définir un élément nommé (une valeur ou une fonction) on utilise le constructeur 'let' suivit d'un identifiant :
# let my_item = 128 ;;
val my_item : int = 128
Ci-dessus l'élément est de type entier int, il pourra donc être utilisé partout où un entier int est attendu. OCaml, en réponse à l'entrée, indique ici en plus le nom de l'élément. (Ce nom ne peut pas commencer par une majuscule ou un chiffre.)
Vérifiez que le lien a bien été défini :
# my_item ;;
- : int = 128
L'expression définie ci-dessus n'est pas à proprement parler ce que l'on appelle une variable dans d'autres langages (on pourrait les voir comme des variables constantes), car sa valeur ne peut être "réellement modifiée" (en tout cas pour les types fonctionnels d'OCaml), même s'il est possible d'en créer une nouvelle portant le même nom (et ce même à partir de la précédante) :
# let my_item = my_item * 2 ;;
val my_item : int = 256
Si vous faites le choix de la programmation fonctionnelle, vous préférerez éviter ce genre de chose car ce genre de programmation ressemble fort à du style impératif. Si lors d'une modification ultérieure du programme vous supprimez la "redéfinition", ce sera donc la précédante qui sera utilisée, et cela peut potentiellement être inapproprié.
Dans le cas ci-dessus certains programmeurs fonctionels recommandent d'utiliser un nouveau nom plutôt que de créer un homonyme (qui aura d'ailleur peut-être un type différent). Prendre un nom différent ne sera pas plus gourmand (ni en temps, ni en espace).
Les variables modifiables transversalement seront vues ultérieurement, mais si vous faites le choix d'adopter un style fonctionel vous éviterez leur usage ; le fait que leur valeur soit accessible transversalement induit des effets de bords, souvent à l'origine de bugs. Il est pratiquement toujours possible d'utiliser une valeur nommée fonctionnelle plutôt qu'une variable impérative.
Après pour l'écriture de scripts, et dans certains cas un style impératif local à une fonction permet parfois d'écrire du code plus simple à lire et à écrire. Il ne faut pas être trop extrémiste non plus, la simplicité du code étant souvent plus importante.

OCaml fournit plusieurs structures permettant de regrouper des éléments (comme les types primitifs vu en début du didacticiel) : les tableaux (type 'array'), les listes (type 'list') et les tuples.

Les Tableaux

Les tableaux sont de dimension fixe, et ne doivent contenir que des éléments du même type :
# [| 'b'; 'E'; 't'; 'i' |] ;;
- : char array = [|'b'; 'E'; 't'; 'i'|]

# let my_array = [| 1; 5; 7; 12; 64 |] ;;
val my_array : int array = [|1; 5; 7; 12; 64|]
Les tableaux sont bien adaptés pour pouvoir accéder rapidement à un élément (n'importe lequel) à partir de son index dans le tableau :
# my_array.(2) ;;
- : int = 7
L'accès à n'importe lequel des éléments d'un tableau se fait en vitesse constante (ce que l'on appèle 0(1) prononcez "Oh un"), ce qui signifie que ça prend le même temps pour accéder à n'importe lequel des éléments. Ce qui n'est pas le cas avec les listes, pour lesquelles il faut dépiler tous les éléments présents avant celui que l'on souhaite retrouver. Avec les listes, il est donc plus rapide d'accéder à un élément au début de la liste, qu'à la fin.

Les Listes

Les listes ne doivent également contenir que des éléments du même type :
# [ 1; 3; 45; 24; 17; 0; 9 ] ;;
- : int list = [1; 3; 45; 24; 17; 0; 9]
On réalise l'empilement et le dépilement des éléments uniquement au sommet de la pile. Les listes ne sont donc pas de taille fixe comme les tableaux. (elle sont de taille fixe au sens de la programmation fonctionnelle, mais une nouvelle liste peut être créée facillement avec une taille différente, et les éléments communs seront partagés au niveau de la mémoire, et non dupliqués)
# let my_list = [ 3; 4; 5 ] ;;
val my_list : int list = [3; 4; 5]

# 8 :: my_list ;;
- : int list = [8; 3; 4; 5]
Elle sont bien adaptées pour réaliser des piles d'éléments, dans lesquelles on va venir empiler et dépiler des éléments au sommet de la pile. (Pensez pour cela à une pile d'assiette, au sommet de laquelle on vient ajouter les assiettes lavées, puis au sommet de laquelle on va venir prendre des assiettes propres lorsqu'on en a besoin.)
L'empilement et le dépilement d'éléments peuvent être réalisés respectivement à partir et jusqu'à la liste vide , laquelle est définie comme suit:
# [] ;;
- : 'a list = []
(Le « 'a » indique alors que le type est indéfini.)
L'opérateur d'empilement est '::' :
# 3 :: [] ;;
- : int list = [3]

# 3 :: 2 :: 1 :: [] ;;
- : int list = [3; 2; 1]
Les listes sont bien adaptées lorsque ses éléments doivent être parcourus de manière séquentielle, et ordonnée.

Les Tuples

Les tuples permettent de regrouper des éléments qui peuvent être de type différent. Ils regroupent généralement un petit nombre d'éléments. Il sont utilisés par exemple lorsqu'une fonction doit retourner plusieurs éléments (qui seront alors regroupés dans un tuple).
# (3, 'F') ;;
- : int * char = (3, 'F')

# let a = 3 ;;
val a : int = 3
# let b = 'F' ;;
val b : char = 'F'

# let (a, b) = (b, a) ;;
val a : char = 'F'
val b : int = 3

# a ;;
- : char = 'F'
# b ;;
- : int = 3
Un tuple peut contenir n'importe quoi, un nombre, une chaîne de caractère, une liste, un tableau ou même une fonction :
# (12.5, "www.ifrc.org", float_of_int) ;;
- : float * string * (int -> float) = (12.5, "www.ifrc.org", <fun>)
Et comme nous l'avons dit, en OCaml tout a un type, même les fonctions ont leur type. Nous allons voir cela plus bas.
Le fait que les fonctions puissent être placée dans des structures et passées en paramètre de fonction, on dit que les fonctions sont des éléments de première classe ("first class citizen functions" en anglais).

Les Fonctions

L'environnement OCaml fournit par défaut un ensemble de fonctions contenues dans sa bibliothèque standard, composée de plusieurs "modules". Le nom des modules commence toujours par une majuscule, par exemple le module String qui fournit des fonctions pour manipuler les chaînes de caractères. Une des fonctions fournies par le module String est la fonction 'length'. Pour pouvoir l'utiliser il faut soit faire précéder le nom de la fonction par le nom du module, soit ouvrir le module 'String' au préalable. Voici les deux méthodes :
# String.length "OCaml" ;;
- : int = 5

# open String ;;
# length "OCaml" ;;
- : int = 5
Une fois ouvert, les fonctions d'un module sont accessibles dans toute la suite de la session ou du fichier de code source.
OCaml est un langage à expression. Chaque élément est une expression, et chaque expression a un type. Ceci s'expérimente ainsi en tapant juste le nom d'une fonction, le type de la fonction est alors indiqué :
# String.length ;;
- : string -> int = <fun>
Le type (ou la signature) d'une fonction est en quelque sorte ce que l'on appelle le prototype d'une fonction dans d'autres langages, mais ce n'est pas totalement équivalent en raison des possibilités d'application partielle d'OCaml qui seront vues ultérieurement.
Ici on voit que la fonction 'length' prend un argument de type 'string' (chaîne de caractère) et qu'elle renvoie un élément de type 'int' (entier).
Avant d'utiliser une fonction, pour savoir quelle sorte de paramètre lui passer, vous pouvez vérifier sa signature dans l'interpréteur interactif ocaml :
[guest@localhost ˜]$ ocaml
        OCaml version 4.11.1
 
# float_of_int ;;
- : int -> float = <fun>
 
# int_of_char ;;
- : char -> int = <fun>
 
# print_string ;;
- : string -> unit = <fun>
Ces fonctions font partie du module 'Stdlib' qui est le module ouvert par défaut (il n'y a pas besoin de l'ouvrir pour en utiliser ses fonctions).
Si vous souhaitez utiliser une ancienne version d'OCaml le module 'Stdlib' se nommait 'Pervasives' avant.
Nous avons également évoqué le module String juste avant, chaque module a une page de documentation dans le manuel d'OCaml.
Cependant pour utiliser certaines de ces fonctions, vous aurez probablement besoin d'en connaître un petit peu plus sur OCaml avant, donc lisez la suite.

Pour définir une fonction nommée on utilise le constructeur 'let', comme pour définir une valeur :
# let step a = a + 2 ;;
val step : int -> int = <fun>

# step 6 ;;
- : int = 8
OCaml étant un langage à expression il n'est point besoin d'utiliser une instruction 'return' comme dans d'autres langages, on donne directement l'expression qui doit être renvoyée.

Dans un programme ou un script, nous avons souvent besoin de pouvoir exécuter certaines parties de codes plutôt que d'autres. OCaml offre plusieurs structures de contrôle pour cela.
(Si vous connaissez déjà des langages de programmation ou de script, vous serez peut-être un peu dépaysé par celles d'OCaml.)

Condition if

La façon la plus simple d'utiliser la structure 'if' ("si" en français), est de lui fournir une comparaison de ce genre :
# 2 > 3 ;;
- : bool = false
ici 2 n'est pas supérieur à 3 donc le booléen 'false' est retourné.
(Pour l'égalité utilisez de préférence « = » à « == » sauf si vous êtes sûr que vous recherchez strictement l'égalité physique de la zone de RAM pointée.)
La structure de contrôle 'if' doit obligatoirement utiliser le type booléen (bool) :
# if 2 > 3 then "first case" else "second case" ;;
- : string = "second case"
On peut développer l'expression précédente :
# let condition = (2 > 3) ;;
val condition : bool = false

# if condition
  then "la condition est vraie"
  else "la condition est fausse"
  ;;
- : string = "la condition est fausse"
Comme les fonctions qui n'utilisent pas d'instruction return, cette structure de contrôle renvoie directement une expression, qui peut être utilisée par exemple pour définir une valeur nommée :
# let lang = "OCaml" ;;
val lang : string = "OCaml"

# let my_item =
    if lang.[0] = 'O'
    then "Objective"
    else "Unknown"
  ;;
val my_item : string = "Objective"
Enchaînement de structures if ("forêts d'else if") :
# let comp a =
    if a = 4
    then "equal"
    else if a > 4
      then "sup"
      else "inf"
  ;;
val comp : int -> string = <fun>

# comp 6 ;;
- : string = "sup"
Afin de garantir le type retourné par la structure if, ses deux branches then et else doivent impérativement retourner des éléments de même type (ce qui est donc le type de l'ensemble de l'expression).
Le seul cas où il n'y a pas besoin de spécifier la seconde branche else c'est lorsque la première branche est de type « unit ».
# let is_equal_to_5 this =
    if this = 5
    then print_endline "this is equal to 5"
  ;;
val is_equal_to_5 : int -> unit = <fun>

# is_equal_to_5  3 ;;
- : unit = ()

# is_equal_to_5  5 ;;
this is equal to 5
- : unit = ()

Les Boucles

Boucle for

# for var = 2 to 6 do
    print_endline ("var value is " ^ (string_of_int var))
  done
  ;;
var value is 2
var value is 3
var value is 4
var value is 5
var value is 6
- : unit = ()
Ici nous voyons apparaître comme type de retours pour l'ensemble de l'expression le type « unit ». En effet la structure de contrôle for correspond au type de programmation impérative, et l'ensemble de l'expression contenue à l'intérieur de la structure 'for' doit être de type « unit ».
Pour placer plusieurs instructions à l'intérieur d'une boucle for, utilisez le séparateur point virgule ';' entre chaque instruction comme ceci :
# for var = 2 to 6 do
    print_string "var value is " ;
    print_int var ;
    print_newline ()
  done
  ;;
var value is 2
var value is 3
var value is 4
var value is 5
var value is 6
- : unit = ()
Pour éviter le style impératif de cette structure de contrôle, OCaml fournit une alternative offrant d'autres possibilités, adaptée à un style de programmation fonctionnelle.

Les Boucles avec des Fonctions

Il est possible de réaliser des boucles grâce à des fonctions qui s'appellent elles-mêmes, ce qui réalise la boucle.
Il faut ajouter le mot-clef 'rec' (pour récursif) lors de la définition de ces fonctions.
# let rec loop a =
    print_int a;
    print_newline ();
    if a > 0 then
      loop (a - 1)
  ;;
val loop : int -> unit = <fun>

# loop 5 ;;
5
4
3
2
1
0
- : unit = ()
Le grand danger cependant avec des boucles réalisées de cette manière est d'en écrire une qui ne s'arrête jamais.
Il faut donc prêter un soin tout particulier à la condition d'arrêt.
Dans l'exemple ci-dessus, la fonction s'appelle elle-même avec un argument sans cesse inférieur, il nous est donc possible de réaliser le rebouclage que lorsque l'argument est supérieur à une certaine valeur, puisqu'il descendra forcément à un moment en dessous de celle-ci.

match

Dans son utilisation basique la plus rudimentaire, cette structure de contrôle pourrait être vue comme un équivalent de la structure de contrôle « switch » que l'on connait dans d'autres langages de programmation et de script.
(À ceci prêt que « match » permet d'aller bien au delà.)
La structure de contrôle 'match' permet de diriger l'exécution du programme en fonction de différentes valeures, ou motifs.
Voici un exemple simple :
# let v = 2 ;;
val v : int = 2

# match v with
  | 1 -> "un"
  | 2 -> "deux"
  | 3 -> "trois"
  | _ -> "je ne sais pas compter au delà de trois"
  ;;
- : string = "deux"
Ici la valeur de 'v' est 2, l'élément correspondant après la flèche est donc retourné.
L'englobement du « match » doit toujours être exhaustif, d'une part, et toutes les expressions suivant chaque englobement doivent être de même type. Ce type sera ainsi le type de l'ensemble de l'expression « match ». (encore cette fameuse histoire de typage fort destiné à assurer le bon fonctionnement de votre code).
Le dernier englobement « _ » peut être utilisé si aucune des autres entrées précédantes ne correspond.
(L'englobement « _ » est à peu près équivalent de la directive « default » des structures 'switch' d'autres langages.)
L'englobement est parfois exhaustif sans celui-ci (voir l'exemple à la fin).
Précédemment nous avions vu comment empiler un élément à une liste. Voici comment en dépiler un :
# let li = [ 2; 8; 5; 11; 32 ] ;;
val li : int list = [2; 8; 5; 11; 32]

# match li with
  | head :: tail -> tail
  | [] -> []
  ;;
- : int list = [8; 5; 11; 32]
(* Dans le jargon, on désigne le premier élément d'une liste comme
   sa tête (head en anglais), et le reste comme sa suite (tail).
   Ici l'expression renvoie la suite de la liste nommée « li ». *)

# match li with
  | head :: tail ->
      print_string "the head is ";
      print_int head;
      print_newline ();
  | [] ->
      print_endline "the list is empty";
  ;;
the head is 2
- : unit = ()
(ici on voit aussi apparaître du commentaire entre les séquences '(*' et '*)')
En utilisant cette possibilité de désempilement au sein d'une boucle faite avec une fonction récursive, cela permet alors de parcourir tous les éléments d'une liste.
# let li = [2; 3; 5; 7] in
  let rec loop li =
    match li with
    | head :: tail ->
        print_int head;
        print_newline ();
        loop tail;
    | [] ->
        print_endline "End Of List";
  in
  loop li
  ;;
2
3
5
7
End Of List
- : unit = ()

"Foreach"

(Iteration sur les listes et les tableaux)

Si vous connaissez d'autres langages et que vous recherchez un équivalent de la structure foreach, la même chose peut-être réalisée pour parcourir tous les éléments d'une liste avec les fonctions « iter » (impératif) et « map » (fonctionnel) du module 'List' de la librairie standard d'OCaml.
# let for_each item = item * 2 ;;
val for_each : int -> int = <fun>

# let my_list = [ 5; 1; 22; 3; 16 ] ;;
val my_list : int list = [5; 1; 22; 3; 16]

# List.map  for_each  my_list ;;
- : int list = [10; 2; 44; 6; 32]
Ici la fonction for_each est appliquée successivement à chaque élément de la liste my_list.
Ainsi la fonction prend en argument un des éléments de la liste (qui doit donc avoir le même type que l'arguement de la fonction), et les résultats successifs de la fonction sont empilés dans une nouvelle liste qui est le résultat global de l'appel à List.map for_each my_list. Cette opération fonctionne donc comme un filtre.
Si la fonction for_each n'est utilisée qu'à un seul endroit du code, elle peut être définie localement au sein de l'expression en cours, plutôt que globalement. (On pourrait tenter la comparaison avec les fonctions inline de C++) Il faut alors la terminer par 'in' à la place de ';;' comme ceci :
# let each item =
    item + 4
  in
  List.map each [10; 50; 30; 80; 20]
  ;;
- : int list = [14; 54; 34; 84; 24]
Elle ne sera alors disponible et accessible que dans l'expression en cours, comme vous pouvez le vérifier :
# each ;;
Unbound value each
Une fonction anonyme peut aussi être utilisée :
# List.iter
    (fun i -> print_int i ; print_newline ())
    [ 8; 64; 2; 16 ]
  ;;
8
64
2
16
- : unit = ()
 
# let high_level_languages = [
    "Lisp";
    "Perl";
    "Python";
    "PHP";
    "Ruby";
    "OCaml"
  ] in
  List.iter (fun lang -> print_endline lang) high_level_languages
  ;;
Lisp
Perl
Python
PHP
Ruby
OCaml
- : unit = ()
Pour parcourir les éléments d'un tableau au lieu d'une liste, il existe deux fonctions semblables « iter » et « map » dans le module 'Array'.

Les Enregistrements

(ou "record" en anglais)
Ce sont des types qui doivent être déclarés au préalable, c'est à dire définis par l'utilisateur ou dans des modules. Ce type ressemble aux tuples sauf qu'il permet de nommer les différents champs, ce qui en améliore la lisibilité, et le nom des champs permet d'accéder plus facilement qu'avec un tuple à chaque éléments de ce type :
# type personne = { nom: string; telephone: string; } ;;
type personne = { nom : string; telephone : string; }

# let doe = {
    nom = "Doe";
    telephone = "02.30.54.29";
} ;;
val doe : personne = {nom = "Doe"; telephone = "02.30.54.29"; }

# doe.telephone ;;
- : string = "02.30.54.29"

Les Enregistrements : Mise à Jour Immuable

(ou "immutable update" en anglais)
En programmation fonctionnelle les variables ne sont pas sensées changer de valeur, pour obtenir un nouvel enregistrement avec un des champs mis à jour, en OCaml on procède comme suit :
# let doe = { doe with telephone = "02.74.16.48" } ;;
val doe : personne = {nom = "Doe"; telephone = "02.74.16.48"}

Les structures mutables

Les champs déclarés mutable au moment de la définition pourront être modifiés.
Par exemple :
# type personne = { nom : string; mutable age : int } ;;
type personne = { nom : string; mutable age : int; }
(* permettra de modifier le champ age, mais pas le champ nom. *)

# let p = { nom = "Paul"; age = 23 } ;;
val p : personne = {nom = "Paul"; age = 23}

# let anniversaire x = x.age <- x.age + 1 ;;
val anniversaire : personne -> unit = <fun>

# anniversaire p ;;
- : unit = ()

# p ;;
- : personne = {nom = "Paul"; age = 24}

# p.nom <- "Maximilien";;
Error: The record field nom is not mutable


Les Variants

Les variants sont également des types qui doivent être déclarés avec une définition.
Voici un exemple :
# type shape = Square | Circle | Triangle ;;
type shape = Square | Circle | Triangle

# let string_of_shape sh =
    match sh with
    | Square   -> "square"
    | Triangle -> "triangle"
    | Circle   -> "circle"
  ;;
val string_of_shape : shape -> string = <fun>

# string_of_shape Square ;;
- : string = "square"

Les Variants avec arguments

Les variants peuvent également être suivits d'arguments :
(* Définition du type shape avec un variant pour représenter différentes formes géométriques *)
type shape =
  | Square of float            (* carré avec un côté *)
  | Triangle of float * float  (* triangle avec une largeur et une hauteur *)
  | Circle of float            (* cercle avec un rayon *)

(* Exemples de formes géométriques *)
let square = Square 5.0
let triangle = Triangle (4.0, 6.0)
let circle = Circle 3.0

Mise en garde contre les tableaux

Attention les tableaux contrairement aux listes sont des structures de type impérative dont les éléments sont modifiés en place, et donc avec ce que l'on appel des effets de bord. Si une fonction en modifie un élément, cette modification prendra effet transversalement, comme le montre cet exemple :
# let arr = [| 1; 2; 3; 4; 5 |] ;;
val arr : int array = [|1; 2; 3; 4; 5|]

# let modif i v =
    arr.(i) <- v;
  ;;
val modif : int -> int -> unit = <fun>

# modif 2 1024 ;;
- : unit = ()

# arr ;;
- : int array = [|1; 2; 1024; 4; 5|]
Ainsi si une fonction modifie un tableau avant de s'en servir pour quelqu'usage que ce soit, celui-ci ne sera pas intact après l'appel à cette fonction. Il faut y penser et y être attentif pour en tenir compte.
Ce type de problème n'existe pas avec les listes qui sont des structures de type fonctionnel. (sauf si la liste contient des éléments modifiables/mutable)
Ainsi si une fonction a besoin d'utiliser une version modifiée d'un tableau ou d'une chaîne de caractère et que le programme en cours aura encore besoin de leur version originale après l'appel de fonction en question, vous pouvez utiliser une copie locale à la place de l'originale avec les fonctions Array.copy ou Bytes.copy.

Les chaînes de caractères

Le même type d'effet de bord pouvaient avoir lieu avec les chaînes de caractères avant ocaml version 4.06 :
# let str = "OCaml" ;;
val str : string = "OCaml"

# let modif str i c =
    str.[i] <- c;
  ;;
val modif : string -> int -> char -> unit = <fun>

# modif str 1 'k' ;;
- : unit = ()

# str ;;
- : string = "Okaml"
Jusqu'à la version 4.01, les chaînes de caractères étaient modifiables.
Un nouveau type bytes fut introduit dans ocaml version 4.02, et la période de transition fut étalée à partir de la version 4.02, et jusqu'à la version 4.14.
A partir de la version 4.06 d'ocaml, les chaînes de caractères sont devenues non-modifiables, par défaut.
A partir de la version 5.00, les chaînes de caractères sont uniquement non-modifiables (plus de mode de compatibilité).
# let s = Bytes.of_string "Hello" ;;
val s : bytes = Bytes.of_string "Hello"

# Bytes.set s 4 'a' ;;
- : unit = ()

# s ;;
- : bytes = Bytes.of_string "Hella"

Récursivité Terminale

La récursivité terminale est une notion très importante à maîtriser en OCaml, car de sa maîtrise dépend la scalabilité de vos programmes.
Voici un exemple de fonction récursive qui calcule la somme d'une suite d'entier de 1 jusqu'à un nombre donné :
# let rec add x =
    if x <= 0
    then 0
    else x + add (x - 1)
  ;;
val add : int -> int = <fun>
À priori, cette fonction paraît correcte, sauf que pour des valeurs élevées il risque d'y avoir une surcharge qui empèchera à cette fonction de renvoyer le résultat.
#  add 10 ;;
- : int = 55

# add 100 ;;
- : int = 5050

# add 10_000 ;;
- : int = 50005000

# add 100_000 ;;
Stack overflow during evaluation (looping recursion?).
Voici ce qui se passe, à la dernière ligne de la fonction, à la suite du else, il y a une addition avec le résultat d'un appel de fonction, donc le calcul ne peut être réalisé aussitôt car il faut d'abord calculer le résultat de cette fonction, le calcul est donc laissé en suspend en attendant. Or il y aura autant de calcul laissés en suspend que d'appel total à la fonction, ce nombre correspondant ici à la valeur du paramètre.
Donc pour cent mille, il y aurra cent mille appels successifs à la fonction et donc cent mille calculs laissés en instance !
La pile d'appel des fonctions (stack) est alors surchargée (overflow).
C'est un peu comme une personne qui aurait du travail à faire et qui chaque jour le remettrait au lendemain, et à la fin il se retrouverait incapable de réaliser tout le travail accumulé.
La solution est donc de réaliser le calcul à faire à chaque étape et de passer le résultat à la boucle suivante.
Il faut donc réaliser une fonction auxiliaire 'aux' avec un paramètre additionnel 'acc' où le résultat final s'accumule à chaque boucle :
# let addt x =
    let rec aux x acc =
      if x <= 0
      then acc
      else aux (x - 1) (x + acc)
    in
    aux x 0
  ;;
val addt : int -> int = <fun>

# addt 100_000 ;;
- : int = 5000050000
La nouvelle fonction est alors dite "récursive terminale", car dans la récursivité le résultat est donné en position terminale, ici au niveau du 'then', alors qu'avant c'était le bout de la chaîne le résultat étant situé au niveau de l'addition (lors de la dernière boucle).
Dans la première version de la fonction, le then renvoyait un 0, c'est donc avec cette valeur que la variable acc est initialisée lors de l'appel à la fonction auxiliaire aux.

Voici un autre exemple de fonction récursive non terminale. Cette fonction produit une liste d'entiers entre deux entiers donnés :
# let rec up_to m n =
    if m > n
      then up_to n m
      else if m = n
        then [m]
        else m :: up_to (m+1) n
    ;;
val up_to : int -> int -> int list = <fun>

# up_to 4 12;;
- : int list = [4; 5; 6; 7; 8; 9; 10; 11; 12]
Et voici la version récursive terminale :
# let up_to m n =
  let rec aux m n acc =
    if m > n
    then acc
    else aux m (n-1) (n::acc)
  in
  if m > n
  then aux n m []
  else aux m n []
  ;;
val up_to : int -> int -> int list = <fun>

# up_to 4 12 ;;
- : int list = [4; 5; 6; 7; 8; 9; 10; 11; 12]
On peut vérifier la capacité de ces deux fonctions avec
ignore(up_to 0 100000) ;;
la version récursive terminale est effectivement plus performante.
D'après le manuel d'OCaml, il faut utiliser une version récursive terminale dès lors que le nombre d'éléments des listes à traiter peut dépasser les dix milles (environ).

Script et exécutables

Après avoir utilisé le top-level, maintenant voyons comment créer des scripts et des exécutables. Pour cela ouvrez un nouveau fichier avec l'extension de fichier .ml correspondant aux fichiers source OCaml, et écrivez une simple instruction, comme par exemple :
print_endline "Hello World!" ;;
Pour exécuter ce fichier en tant que script, il suffit ensuite de lancer l'interpréteur en ligne de commande avec le script en argument comme ceci :
ocaml hello.ml
Si vous utilisez un système de type Unix comme Linux, MacOS, Cygwin ou Chromebook, il vous est également possible de pouvoir exécuter votre script comme un exécutable sans préciser l'interpréteur sur la ligne de commande. Voici comment procéder, premièrement ajoutez à la première ligne du script le chemin de l'interpréteur comme ceci :
#!/usr/bin/env ocaml
ensuite rendez le exécutable en lui ajoutant les droits d'exécution :
chmod a+x hello.ml
vous pouvez alors exécuter directement votre script comme n'importe quel exécutable :
./hello.ml
Et pour ce qui est de créer des exécutables pour de meilleures performences, vous pouvez compiler votre fichier source au choix en code natif ou en byte-code. La compilation en byte-code a pour avantage que l'exécutable pourra être exécuté indépendament sur n'importe quel système d'exploitation où la machine virtuelle OCaml est installée. La machine virtuelle est un interpréteur qui interprète le code binaire dans lequel a été compilé le fichier source.
La compilation en code natif a elle pour avantage des performences optimums (comparable à du C ou du C++).
Ainsi pour compiler en byte-code, utilisez le compilateur correspondant :
ocamlc  hello.ml  -o hello.byte
./hello.byte
Et de même pour compiler un exécutable en code natif :
ocamlopt  hello.ml  -o hello
./hello
Idem pour les utilisateurs de Windows nécessitant une extention de fichier :
ocamlopt  hello.ml  -o hello.exe
./hello.exe

Main

Point d'entrée du programme

Pour organiser les éléments dans un script ou un programme ocaml, il est recommandé de procéder comme suit. Placer l'ensemble des fonctions au début du programme, suivit à la fin du point d'entrée, où l'exécution du programme commencera. (si une fonction est définie mais sans être appelée, elle ne réalisera rien).
En C, le point d'entrée est la fonction 'main', en ocaml on place un 'let' suivit d'un 'unit', comme dans cet exemple :
let fib n =
  let rec aux a b acc =
    if a > n then (List.rev acc)
    else aux b (a+b) (a::acc)
  in
  aux 0 1 []

let () =
  let n = int_of_string Sys.argv.(1) in
  let li = fib n in
  let s = String.concat " " (List.map string_of_int li) in
  print_endline s;
;;
Ce petit programme affiche la suite de Fibonacci, jusqu'à un certain nombre passé en paramètre au programme par la ligne de commande :
$ ocaml fib.ml 1000
 0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987
En biologie, la suite de Fibonacci se retrouve dans de nombreux phénomènes naturels, tels que la croissance des plantes, la structure des fleurs, la disposition des feuilles sur une tige, les spirales de coquillages, etc. Elle est utilisée pour modéliser et comprendre ces phénomènes.
Sys.argv est un tableau qui contient les éléments de la ligne de commande qui a lancé le programme.

Les Exceptions

Dans le programme précédent, l'expression int_of_string Sys.argv.(1) est susceptible de produire 2 types d'exceptions :
Exception: Invalid_argument "index out of bounds"
si le programme est appelé sans arguement, ce qui aura pour résultat de tenter d'accéder à un index en dehors des valeurs possibles pour le tableau Sys.argv , ou bien
Exception: Failure "int_of_string".
si l'argument n'est pas convertible en entier, comme par exemple si le programme est invoqué avec :
$ ocaml fib.ml alpha
Avec le couple try / with utilisé avec le caractère d'englobement _, il est possible de renvoyer une valeur alternative quelque soit l'exception rencontrée :
let () =
  let n =
    try int_of_string Sys.argv.(1)
    with _ -> 100
  in
  let li = fib n in
  let s = String.concat " " (List.map string_of_int li) in
  print_endline s;
;;
Il est également possible de réaliser un traitement différent en fonction de l'exception rencontrée :
let () =
  let n =
    try int_of_string Sys.argv.(1)
    with
    | Invalid_argument _ -> 100    (* default value *)
    | Failure _ ->    (* exit with an error message *)
        prerr_endline "please provide an integer";
        exit 1
  in

Voilà pour un premier épisode, il est un peu long et peu amusant pour un début, mais il faux comprendre le typage des expressions avant de pouvoir aller plus loin vers des choses plus évoluées.
Dites-moi si vous l'avez apprécié ou pas, pour que je sache si je dois en faire d'autres ou pas.

Aller plus loin

Si vous aimez apprendre en faisant de petits jeux, vous pouvez vous rendre à cette adresse pour trouver de petits exemples de mimi-jeux en OCaml.
Vous pouvez lire cette excellente "Introduction au langage OCaml" par Maxence Guesdon sous licence CC-by-nc-sa.
cours d'introduction à ocaml par Emmanuel Hainry.
Divers exemples (avec équivalences dans d'autres langages) sur le site RosettaCode.org.
Tutoriel sur comment réaliser un mini snake-game.
Wikibook sur ocaml par divers auteurs : 1 / 2 / 3.
Comment utiliser les structures de type Set, et Map.

Notice d'utilisation

© 2005 2006 2007 2018 Florent Monnier
Ce document est distribué sous licence FDL, ce qui signifie que vous pouvez le redistribuer avec ou sans améliorations du moment d'en conserver le droit d'auteur (copyright) et la licence.
Vous pouvez également utiliser la licence CC-by-sa si vous préférez.

Camellement vôtre !

sommaire
The Caml Language