Translation in Progress
Notice d'utilisation
© 2005 2006 2007 2018 Florent Monnier
This file is provided under
FDL license,
which means that you can reuse it, with or without modifications
as long as the copyright is preserved, and its license.
You can also use the
CC-by-sa
license if you prefer.
You are also invited to send me messages to suggest me improuvements,
or to fix errors.
Introduction about OCaml
OCaml (also called Objective Caml) is a programming and scripting language.
The goal of this document is to provide an introduction about the basic elements
of this programming language.
(Don't try to contact me on this electronic mail anymore, I forgot the password.)
As explained on the website of this programming language, ocaml is
supposed to be a programming language created by the
INRIA.
It is advertised as a programming language with a safety of execution,
and with strongly typed elements.
It is also described as a functional programming language, but not
a pure one as Haskell.
Different Execution modes
OCaml provides several execution modes:
-
Binaries, compiled to natif code
(similar than with the C programing language and with similar performences)
-
With Bytecodes that you run with a virtual machine
(like Java and with equivalent performences)
-
As a script
(like Python, Perl, Ruby or PHP, you will have similar fun)
-
With the Interactive loop, for real-time interaction with the user,
also called the "toplevel"
(similar than the 'python3' command)
All these execution modes will be seen with more details later.
In this tutorial, we first experiment with the top-level.
Different Programming Styles
We can adopt several programming styles with ocaml: functional, imperative, object,
modular, and mix these different programming styles.
The functional style is supposed to be better when it's about to avoid side effects.
It's usually usefull to avoid a lot of global variables, that are difficult to track down.
It also has as advantage to make your functions easier to test.
The drawback is that it sometimes make the code more complicated, when we have to
update or calculate several elements at the same times.
If you want to continue, then find an online interactive loop to begin,
or even install it from:
https://ocaml.org/docs/install.html
or you may install it from the installer provided with your computer or the
device you're using.
The interactive loop
To begin, one of the first step is to get used to the type system of ocaml
which is a quite strong type system. It might feel constraining at the
beginning but it could be of some help, while using it.
Then you can use the interactive loop, called the "toplevel", with the 'ocaml'
command in the console. You should then get a new session with a prompt similar
than this one:
[guest@localhost ˜]$ ocaml
OCaml version 4.12.2
#
Every expression should be followed by two
';
':
;;
In order to understand the type system, we will see the primitive types first,
they are the base elements:
It's also possible to make it easier to make edits in the interactive loop
with the rlwrap
tool, like this:
[guest@localhost ˜]$ rlwrap ocaml
OCaml version 4.12.2
#
This tool offers a read-line interface, which adds edits capabilities in the
interactive loop.
Basic Types
1) The integer type (integer number), try to type in the prompt:
# 4 ;;
The interactive loop will then reply with the type of the expression
that the type system inferred (guess), followed by its value:
- : int = 4
2) floating point numbers (float's):
# 8.6 ;;
- : float = 8.6
The dot is required so that the type system knows it's a float, even if there
is nothing after:
# 12. ;;
- : float = 12.
Otherwise we would get an integer:
# 12 ;;
- : int = 12
3) The boolean type, which value is either true
or false
:
# true ;;
- : bool = true
# false ;;
- : bool = false
4) The character type (one, and only one character.)
They are provided between single quotes:
# 'e' ;;
- : char = 'e'
5) The string type, contains zero, or several characters.
They are provided between double quotes:
# "OCaml" ;;
- : string = "OCaml"
The concatenation operator for strings is:
# "OC" ^ "aml";;
- : string = "OCaml"
You can access to any character of a string like this:
# "OCaml".[2] ;;
- : char = 'a'
As you can see, the type of the element returned is the character type
char
as you would expect.
The index starts at 0
(zero) for the first character, and
the last one is n-1
, with n
been the number
of chars in the string.
(if the string contains some utf-8 special characters, n might be different
than what you expect, because special characters may take 2 bytes/octets.)
If we try to access to a char beyond the last one, an exception will be raised:
# "OCaml".[26] ;;
Exception: Invalid_argument "index out of bounds".
Defining, raising and catching exceptions will be seen below.
6) The type « unit » :
# () ;;
- : unit = ()
This type is a kind of empty entity.
It is used for example when a function doesn't need any parameters.
This is also the default returned type when a function does something
but doesn't have anything to return.
This type is not exactly the same than
« void » and « NULL »
of other languages, because its use is not exactly the same.
A function that doesn't have anything to return, will return a single
« unit » :
# print_endline "OCaml" ;;
OCaml
- : unit = ()
Here the function does something (displaying text, which is considered as a "side effect"),
but doesn't have anything to return, so the default returned value is a single "unit".
Transtyping
OCaml features a strong type system, because it is believed that a statically
strong type system, is a great help, in order to avoid programming errors.
OCaml will not do any "implicit type coercion", because it is believed that it
can lead to incidious errors, so ocaml programmers have to do it explicitly.
("type coercion" is converting a type to another compatible type, like for example
converting an integer number into a float number, or the opposite.)
To convert an integer number into a floatting point number, you can do:
# float_of_int 16 ;;
- : float = 16.
And the opposite conversion:
# int_of_float 12.0 ;;
- : int = 12
If you want to get the ascii-code (character encoding) of a character, you can do:
# int_of_char 'A' ;;
- : int = 65
And the reverse:
# char_of_int 97 ;;
- : char = 'a'
In a similar way, fr arithmetic operations, the numbers have to be of the same type:
# 4 + 6 ;;
- : int = 10
For arithmetic operations with floats, operators have to be suffixed with a dot:
# 2.4 *. 1.2 ;;
- : float = 2.88
You can mix both using a coercion from a type to another:
# (float_of_int 9) /. 1.2 ;;
- : float = 7.5
In order to define a "named-value" (some data, or a function),
we can use the let
construct follow by a name
that will identify the defined value:
# let my_item = 128 ;;
val my_item : int = 128
Here the element that we define is an integer number, and it will be possible
to use it everywhere after its definition (where an integer type is expected),
using the name that we used to define it.
The ocaml top-level, after this definition, will reply with this name, and
its associated value, and its infered type.
(This name can not start with an upper-case character, or a number.)
You can check that the link to the value has properly being defined to
its value by calling its name:
# my_item ;;
- : int = 128
Most often, the word "variable" is avoided, because ocaml is a functional
programming language, and functional programming languages try to avoid
mutability, so the variable should not vary. But ocaml is an "impure"
functional programming language, so it is possible to introduce mutable
elements. It is possible to use the terms: "mutable named-value", in this
case in stead of "variable". It is often easier to use the same word than
in other programming languages ("variable") though.
# let my_item = my_item * 2 ;;
val my_item : int = 256
It is also possible to redefine another "named-value" with the same name, this
is called: "shadowing", because the previous definition is hidden by the new
one.
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.
Arrays
Arrays can group together, zero, one, or several elements of the same type.
The number of elements in ocaml arrays can not change.
# [| '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|]
Arrays are the right data structure to access any of their element, in constant time, from an index:
# my_array.(2) ;;
- : int = 7
L'accès à n'importe quel élément d'un tableau se fait en vitesse constante
The time that will take the access of one element, is quite small, and is always the same for every
elements of the array. This is what we call "constant-time", and in computer complexity-study it is
written O(1) (pronouced "Oh, one"), and this is not the same for lists, where to
access one of its element, we have to pop all the previous elements. This is why with lists, we say
that the time needed to access an element, is O(n), where n
is the
position of the element in the list.
Lists
OCaml lists should also contain only elements of the same kind/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.
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).
Tuples can group elements that can be of several different types. They usually
group a small number of elements together.
They are used for example when a function can return several elements.
# (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
Tuples can contain anything, numbers, chars, strings, lists, arrays,
and even functions!
# (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 ç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).
Functions
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
Once opened, the functions of a module are accessible in the current
repl session, or source code file, or code block.
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' ("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"
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 = ()
Loops
For loops
# 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.
Loops with functions
It's possible to create loops with functions calling themself.
We need to add the keyword 'rec'
(which stands for recursive) when we define these functions.
# 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à.)
Here is a simple example:
# 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 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";
"Nim"
] in
List.iter (fun lang -> print_endline lang) high_level_languages
;;
LISP
Perl
Python
PHP
Ruby
OCaml
Nim
- : 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'.
Using Records
OCaml records are data structures that should be defined before to be used.
This data structure is similar than tuples, except than every field can be
accessed with a name (the field-name).
The OCaml programming language provides the record data-structure to group
several elements of different type kinds.
They need to be defined, (with the key-word type
), before they
can be used.
The definition, defines every field, with a field name, and its type kind.
.
# type person = { name: string; telephone: string; } ;;
type person = { name : string; telephone : string; }
# let doe = {
name = "Doe";
telephone = "FD.CE.AF";
} ;;
val doe : person = {name = "Doe"; telephone = "FD.CE.AF"; }
# doe.telephone ;;
- : string = "FD.CE.AF"
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 nous procédons comme suit :
# let doe = { doe with telephone = "C85.5C8" } ;;
val doe : person = {nom = "Doe"; telephone = "C85.5C8"}
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 voisin..."
;;
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
.
Terminal Recursion
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 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 100_000 ;;
- : 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, MacOS ou Cygwin, 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.
Aller plus loin
Divers exemples (avec équivalences dans d'autres langages) sur le site
RosettaCode.org.
Camellement vôtre !
GNU/FDL
© 2005 2006 2007 2018 2025 Florent Monnier