Notice d'utilisation

© 2005 2006 2007 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.
Vous êtes également invité à m'écrire pour me suggérer des améliorations, ou simplement pour me soumettre des suggestions
 
Merci à Xavier Hienne pour la relecture.

Introduction à OCaml

OCaml (ancièment Objective Caml) est un langage de script et de programmation, ce document à pour but d'en proposer une introduction 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, n'hésitez pas à me le signaler, ou à me demander des précisions sur certains points, ou au contraire à en apporter pour enrichir ce didacticiel
 
Tout d'abord une courte présentation d'OCaml. Il s'agit d'un projet initié par l'INRIA.
Le langage OCaml fournit une exceptionnelle sureté d'exécution notament grâce au fait que tout ses éléments soient « TRÈS FORTEMENT TYPÉS ».
Ce dernier point requière un certain temps d'adaptation au début, mais est une aide très précieuse par la suite.


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 d'interaction.

Les styles de programmation

OCaml offre deplus 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é au typage fort, 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 : https://ocaml.org/docs/install.fr.html ou bien à l'aide de l'installeur fournit par votre distribution.

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.
[blue_prawn@azur ˜]$ 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.

Les types primitifs

1) Le type entier (décimal), tapez au prompt :
# 4 ;;
OCaml vous indiquera alors en réponse le type de l'expression que vous venez d'entrer, suivit de sa valeur :
- : int = 4
2) Le type nombre à virgule (flottante) :
# 8.6 ;;
- : float = 8.6
Le point est obligatoire pour indiquer le type flottant, même s'il n'y a rien après :
# 12. ;;
- : float = 12.
Sinon nous aurions 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 l'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. 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 aller bien au delà.
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 ».

Transtypage

Comme il est dit et répété OCaml est fortement typé pour éviter les erreurs de programmation. Il n'est ainsi pas possible de faire le transtypage (convertir le type) implicitement d'un élément, 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
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.

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 adaptés pour pouvoir accéder à un élément (n'importe lequel) à partir d'un index donné :
# my_array.(2) ;;
- : int = 7
L'accès à n'importe quel élément d'un tableau se fait en vitesse constante (ce que l'on appèle 0(1) prononcez "Oh un"), ce qui n'est pas le cas avec les listes, pour lesquelles il faudrait dépiler tous les éléments avant d'accéder à l'élément que l'on souhaite retrouver.

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.
# 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 :
# [] ;;
- : 'a list = []
(Le « 'a » indique alors que le type est indéfini.)
Les listes sont bien adaptées lorsque ses éléments doivent être parcourus de manière séquentielle.

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, un tableau ou même une fonction :
# (12.5, "www.ifrc.org", float_of_int) ;;
- : float * string * (int -> float) = (12.5, "ICCF.nl", <fun>)
Et comme nous l'avons dit, en OCaml tout a un type, même les fonctions ont leur type. Nous allons voir ça 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 librairie 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é :
# length ;;
- : string -> int = <fun>
Le type 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' (chaine de caractère) et qu'elle renvoie un élément de type 'int' (entier).
OCaml étant fortement typé, il est ainsi utile de vérifier le type d'une fonction avant de l'utiliser, afin de bien avoir en tête le type des paramètres à lui passer, et le type du ou des éléments ou de l'expression de retour de la fonction. On peut ainsi vérifier le type des fonctions utilisées au tout début de cette introduction à OCaml :
# 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', 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"
Enchainement 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 elle ne peut contenir que des éléments dont la valeur de retours est le 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 beaucoup plus de possibilités, et surtout une sureté très largement accrue.

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êtent 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à.)
Voici un exemple simple :
# let var = 2 ;;
val var : int = 2

# match var with
  | 1 -> "un"
  | 2 -> "deux"
  | 3 -> "trois"
  | _ -> "je ne sais pas compter au delà de trois"
  ;;
- : string = "deux"
Ici la valeur de var 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 « _ » est utilisé si aucune des entrées le précédant ne correspond.
(L'englobement « _ » est à peu près équivalent de la directive « default » des structures 'switch' d'autres langages.)
L'englobement peut parfois être 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 la suite comme sa queue (tail).
   Ici l'expression renvoie la queue 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 "The End";
  in
  loop li
  ;;
2
3
5
7
The End
- : 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

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 ville = { nom: string; superficie: float; population: int } ;;
type ville = { nom : string; superficie : float; population : int; }

# let paris = {
    nom = "Paris";
    superficie = 105.40;  (* km2 *)
    population = 2_101_816;  (* habitants *)
} ;;
val paris : ville = {nom = "Paris"; superficie = 105.4; population = 2101816}

# paris.population ;;
- : int = 2101816

Les Variants

Les variants sont également des types qui doivent être déclarés avec une définition.
Voici un exemple librement inspiré d'un exemple de David MENTRE :
# type chocolat = Chocolat_noir | Chocolat_blanc | Chocolat_au_lait ;;
type chocolat = Chocolat_noir | Chocolat_blanc | Chocolat_au_lait

# let quoi_en_faire tablette =
    match tablette with
    | Chocolat_noir    -> "Je le mange goulûment !"
    | Chocolat_au_lait -> "Je le refile à ma petite soeur."
    | Chocolat_blanc   -> "Je le refile à mon petit cousin... (discrètement)"
  ;;
val quoi_en_faire : chocolat -> string = <fun>

# quoi_en_faire Chocolat_noir ;;
- : string = "Je le mange goulûment !"

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.
Le même type d'effet de bord peuvent avoir lieu avec les chaînes de caractères :
# 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"
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 String.copy ou Array.copy.

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 10000 ;;
- : int = 50005000

# add 100000 ;;
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 aurra 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 100000 ;;
- : int = 705082704
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 celà 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 ou MacOS, 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.bin
./hello.bin
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

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.
Vous pouvez vous rendre à cette adresse pour trouver de petits exemples de mimi-jeux.

Camellement vôtre !
GNU/FDL © 2005 2006 2007 Florent Monnier
 

sommaire
The Caml Language