Vous savez qu'il peut y avoir des erreurs durant l'exécution du programme, une division par 0, un appel à
List.tl
sur la liste vide, etc. Nous avons vu aussi comment provoquer une erreur qui interrompt le calcul en affichant un message d'erreur avec la fonction invalid_arg
. La fonction failwith
est une variante d'invalid_arg
.
let () =
print_string "hello";
print_newline ();
failwith "exception!"; (* <- le calcul s'arrête ici *)
print_string "world";
print_newline()
File "[2]", line 4, characters 2-23: 4 | failwith "exception!"; (* <- le calcul s'arrête ici *) ^^^^^^^^^^^^^^^^^^^^^ Warning 21: this statement never returns (or has an unsound type.)
hello
Exception: Failure "exception!".
Raised at file "stdlib.ml", line 29, characters 22-33
Called from unknown location
Called from file "toplevel/toploop.ml", line 208, characters 17-27
D'autres exemples d'exceptions sont
Match_failure
: erreur levée lorsqu'une définition par cas est incomplèteDivision_by_zero
: comme son nom l'indique...Assert_failure
: erreur levée par un assertmatch 2 with 0 -> true | 1 -> false (* pas de else ... *)
File "[3]", line 1, characters 0-35: 1 | match 2 with 0 -> true | 1 -> false (* pas de else ... *) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Warning 8: this pattern-matching is not exhaustive. Here is an example of a case that is not matched: 2 File "[3]", line 1, characters 0-35: 1 | match 2 with 0 -> true | 1 -> false (* pas de else ... *) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Warning 8: this pattern-matching is not exhaustive. Here is an example of a case that is not matched: 2
Exception: Match_failure ("[3]", 1, 0).
Called from file "toplevel/toploop.ml", line 208, characters 17-27
1 / 0
Exception: Division_by_zero.
Raised by primitive operation at unknown location
Called from file "toplevel/toploop.ml", line 208, characters 17-27
assert (1 = 0 + 0)
Exception: Assert_failure ("[5]", 1, 0).
Called from file "toplevel/toploop.ml", line 208, characters 17-27
try...with...
¶Ces "erreurs" n'en sont pas toujours, elles peuvent être volontaires. Par exemple, dans une fonction, je peux demander un nombre à l'utilisateur et supposer qu'il fournira bien un nombre; si jamais il tape autre chose, ma fonction fera une erreur, mais ce n'est pas grave, je vais la rattraper ailleurs. Je me concentre sur le cas habituel dans la fonction et je traite le cas exceptionnel ailleurs.
let affiche_inverse () =
let n = read_int () in print_int (1 / n); print_newline()
val affiche_inverse : unit -> unit = <fun>
Si j'appelle affiche_inverse ()
, le toplevel me demande de saisir quelque chose.
1
, le nombre 1
s'affiche0
, j'obtient le message Exception: Division_by_zero
toto
, j'obtient le message Exception: Failure "int_of_string"
. Créons maintenant une version "sans erreur" de la fonction
affiche_inverse
let affiche_inverse_sans_erreur () =
try affiche_inverse () with
| Division_by_zero -> print_string "Vous avez tapé 0."
| Failure(msg) -> print_string "Vous n'avez pas tapé un entier."
| _ -> print_string "Une erreur inconnue s'est produite"
val affiche_inverse_sans_erreur : unit -> unit = <fun>
Si j'appelle maintenant affiche_inverse_sans_erreur ()
et que je tape 0
, le toplevel ne me signale plus d'exception. Normal, je l'ai rattrapée!
raise
¶Pour provoquer la levée d'une exception, la méthode la plus générale est d'utiliser la fonction raise
avec pour argument l'exception que l'on souhaite lever. Les exceptions ont des noms qui commencent par une majuscule, par exemple Failure
, Invalid_argument
, Not_found
, etc. On peut par exemple redéfinir failwith
et invalid_arg
comme suit.
let invalid_arg msg = raise (Invalid_argument msg)
val invalid_arg : string -> 'a = <fun>
let failwith msg = raise (Failure msg)
val failwith : string -> 'a = <fun>
let myassert test = if not test then raise (Failure "assertion ... on line ... failed ")
val myassert : bool -> unit = <fun>
L'exception Not_found
ne prend pas d'argument. On l'utilise typiquement quand une recherche échoue.
let index x liste = (* index x liste = index de la première apparition de x dans liste *)
let r = ref liste in
let i = ref 0 in
while (!r <> []) && (List.hd !r <> x) do
r := List.tl !r;
i := !i + 1
done;
if (!r <> []) then !i else raise Not_found
val index : 'a -> 'a list -> int = <fun>
let mem x liste = (* mem x liste = true si x apparait dans liste *)
try
ignore (index x liste); (* ignore:'a -> unit, nous évite un warning *)
true
with
| Not_found -> false
val mem : 'a -> 'a list -> bool = <fun>
On n'est pas obligé de rattraper une exception à l'endroit où elle a été levée.
let f1 () = raise Not_found
let f2 () = print_string "hello," ; f1 ()
let f3 () = try f2 () with _ -> print_string " world!"; print_newline()
let () = f3 ()
val f1 : unit -> 'a = <fun>
val f2 : unit -> 'a = <fun>
val f3 : unit -> unit = <fun>
L'exception remonte tous les appels de fonction jusqu'à trouver un bloc try/with
qui la rattrape. Si le bloc try/with
n'a pas prévu l'exception, elle passe au travers. Sinon, elle est rattrapée et elle n'est pas propagée plus loin.
try
try
try
raise (Failure "boum")
with
| Not_found -> print_string "ce texte ne s'affichera pas"
with
| Failure(s) -> print_string s;print_string " rattrapé"; print_newline ()
with
| _ -> print_string "ce texte ne s'affichera pas"
hello, world!
- : unit = ()
Failure
, Invalid_argument
, ou encore Not_found
sont des exceptions prédéfinies. On peut cependant déclarer ses propres exceptions et les utiliser ensuite.
exception Toto
boum rattrapé
exception Toto
Toto
- : exn = Toto
Le type exn
des exceptions est un type énuméré un peu particulier. On peut l'étendre avec de nouveaux constructeurs d'exceptions, comme ici Toto
. Ces constructeurs peuvent d'ailleurs prendre des arguments.
exception Myfailure of string
exception Myfailure of string
Examinons le type de raise
raise
- : exn -> 'a = <fun>
Le type de raise permet de lever une exception dans n'importe quel contexte tout en respectant les contraintes de typage: une expression comme raise Toto
a le type polymorphe 'a
.
Réécrivons maintenant la fonction index
avec un "return".
exception Return of int
let return x = raise (Return x)
exception Return of int
val return : int -> 'a = <fun>
let index x liste =
let r = ref liste in
let i = ref 0 in
try
while (!r <> []) do
if List.hd !r = x then return !i;
i := !i + 1;
r := List.tl !r
done;
raise Not_found
with
| Return(n) -> n
val index : 'a -> 'a list -> int = <fun>
let _ = index 3 [0; 1; 3; 2]
- : int = 2
let _ = index 4 [0; 1; 3; 2]
Exception: Not_found.
Raised at file "[20]", line 10, characters 10-19
Called from file "toplevel/toploop.ml", line 208, characters 17-27
Tout programme qui s'exécute a une entrée standard (stdin
) et une sortie standard (stdout
).
Certaines instructions permettent de lire des données sur l'entrée standard et de les écrire sur la sortie standard, nous en avons déjà vu quelques unes:
print_int n
: écrit l'entier n sur la sortie standardprint_float x
, print_string s
, print_newline ()
écrivent aussi sur la sortie standardread_int ()
: lit un entier sur l'entrée standard et renvoie sa valeurread_float ()
, read_line ()
: lisent aussi sur l'entrée standardÉcrivons un programme salutation.ml
qui demande le nom de l'utilisateur et qui lui dit bonjour.
(* salutation.ml *)
let salutation () = begin
print_string "Quel est ton nom, humain? ";
let nom = read_line () in
print_string ("bonjour, " ^ nom);
print_newline()
end
let () = salutation ()
Remarque: les entrées-sorties sont "bufferisées" en OCaml, si l'on omet le print_newline()
on ne verra pas forcément apparaitre "bonjour ..." à la fin de la fonction salutation
.
Pour tester mon programme, il y a trois étapes
ocamlc salutation.ml
depuis un terminal./a.out
Je peux rediriger l'entrée standard et la sortie standard vers des fichiers ordinaires. Par exemple, si je tape
./a.out <nom.txt >msg.txt
et si nom.txt
est un fichier que j'ai créé et qui contient "Etienne" sur la première ligne, j'obtiendrai
"Bonjour, Etienne" dans le fichier msg.txt
Je peux aussi changer le nom du fichier exécutable généré par ocamlc
en utilisant l'option -o
.
ocamlc salutation.ml -o salut
./salut
End_of_file
¶Écrivons maintenant un programme qui calcule la somme des entiers qu'il lit sur l'entrée standard, et qui l'affiche sur la sortie standard. C'est l'exception End_of_file
qui permettra de déterminer le moment auquel on a atteint la fin de l'entrée standard.
(* somme.ml *)
let calcul_somme () = begin
let acc = ref 0 in
try
while true do (* boucle infinie! *)
acc := !acc + read_int ()
done
with
End_of_file -> print_int !acc
end
let () = calcul_somme ()
Pour tester ce programme, je crée un fichier nombres.txt
qui contient des nombres, puis j'exécute ./a.out <nombres.txt
Je peux aussi taper mes nombres dans l'entrée standard liée au terminal, et la fermer avec Crtl+D.
Et si je veux spécifier dans mon programme le fichier nombres.txt
qui m'intéresse?
Il va me falloir créer un canal d'entrée (in_channel
) pour ce fichier, puis utiliser des fonctions plus générales de lecture sur un canal.
Les principales étapes sont les suivantes:
let ic = open_in "nombres.txt"
me permet d'ouvrir le fichier en lecture et d'obtenir le canal d'entrée ic
input_line ic
close_in ic
(* fichier somme2.txt *)
let somme () =
let ic = open_in "nombres.txt" in (* <- OUVERTURE *)
let acc = ref 0 in
let rec iter () =
let s = input_line ic in (* <- LECTURE *)
let n = int_of_string s in
acc := !acc + n;
iter ()
in
try iter () with
| End_of_file ->
begin
print_int !acc;
close_in ic (* <- FERMETURE *)
end
(* pour lancer le programme: let () = somme () *)
val somme : unit -> unit = <fun>
Pour écrire dans un fichier, inversement, il me faut un out_channel
. Les étapes sont sensiblement les mêmes:
open_out
output_string
close_out
let comptine () =
let oc = open_out "comptine.txt" in
for i=1 to 10 do
output_string oc (string_of_int i)
done;
close_out oc
val comptine : unit -> unit = <fun>
Notez que l'entrée standard et la sortie standard sont des canaux comme les autres
stdin
- : in_channel = <abstr>
stdout
- : out_channel = <abstr>
Écrivons maintenant un programme qui remplace chaque ligne de nombres prise dans un fichier "in.txt" par une ligne contenant la somme.
C'est l'occasion de découvrir quelques nouvelles fonctions bien utiles.
La fonction String.split_on_char sep s
permet de découper une chaîne de caractères en une liste de chaînes de caractères en prenant sep
comme caractère de séparation
String.split_on_char ' ' "21 823 87"
- : string list = ["21"; "823"; "87"]
La fonction List.map f [x1; x2; ...; xn]
renvoie la liste [f(x1); f(x2); ...; f(xn)]
List.map int_of_string ["21"; "823"; "87"]
- : int list = [21; 823; 87]
La fonction List.fold_left (++) x0 [x1; x2; ...; xn]
renvoie ((... (x0 ++ x1) ++ x2) ++ ... ++ xn)
List.fold_left (+) 0 [21; 823; 87]
- : int = 931
Revenons à notre programme!
let somme () =
let ic, oc = open_in "alea.txt", open_out "somme.txt" in
let rec iter () =
let s = input_line ic in
let ls = String.split_on_char ' ' s in
let li = List.map int_of_string ls in
let n = List.fold_left (+) 0 li in
let s2 = (string_of_int n) ^ "\n" in
output_string oc s2;
iter ()
in
try iter () with
| End_of_file -> close_in ic; close_out oc
| exn -> (* on relance l'exception si ce n'est pas End_of_file *)
(close_in ic; close_out oc; raise exn)
val somme : unit -> unit = <fun>
|>
d'application inversée¶On peut rendre le programme précédent un peu plus stylé en utilisant l'opérateur |>
d'application de fonction en ordre inversé.
x |> f
$\Leftrightarrow$ f x
.
let somme () =
let ic, oc = open_in "alea.txt", open_out "somme.txt" in
try while true do
input_line ic |> String.split_on_char ' ' |> List.map int_of_string
|> List.fold_left (+) 0 |> string_of_int |> (fun s -> s^"\n") |> output_string oc
done with exn -> close_in ic; close_out oc; if exn<>End_of_file then raise exn
val somme : unit -> unit = <fun>
Vous vous souvenez peut-être des chaînes de format de Python?
print("Il est {}:{}".format(15+2, 3+2))
# affiche "il est 17:05"
OCaml permet de faire quelque chose de similaire avec
Printf.printf "Il est %d:%d" (15+2) (3+2)
Le type de chacun des arguments qui suivent la chaîne de format est indiqué par un "marqueur" dans la chaîne de format
%d
pour un entier%s
pour une chaîne de caractères%f
pour un flottant%c
pour un caractèreLe caractère retour à la ligne s'écrit \n
, mais il ne force pas à vider le buffer. Pour vider le buffer, on peut utiliser %!
.
let caractere_pi = "\xCF\x80" (* le codage sur 2 octets de π en UTF8 *)
let pi = 4. *. atan 1.
let () = Printf.printf "le nombre %s vaut %.2f à %d decimales près.\n%!" caractere_pi pi 2
val caractere_pi : string = "π"
val pi : float = 3.14159265358979312
le nombre π vaut 3.14 à 2 decimales près.
La fonction Printf.fprintf
généralise la Printf.printf
en prenant un out_channel
en argument.
let oc = open_out "toto.txt" in
Printf.fprintf oc "%s" "hello!";
close_out oc
La fonction Printf.sprintf
écrit dans une chaîne de caractères et la renvoie.
let s = Printf.sprintf "le nombre %s vaut %.2f à %d decimales près." caractere_pi pi 2
val s : string = "le nombre π vaut 3.14 à 2 decimales près."
Ce n'est qu'un aperçu, mais vous croiserez dans le manuel de référence:
Scanf
, qui permet de faire des lectures formatées let read_int ()= Scanf.bscanf "%d" (fun n -> n)
Format
, qui généralise encore le module Printf
en permettant de gérer la mise en page du texteCes deux modules sont hors programme! Si vous poursuivez en OCaml plus tard, vous aurez probablement l'occasion de vous y intéresser.