Outils pour utilisateurs

Outils du site


ocaml

Bibliothèque standard

La bibliothèque standard d'OCaml est relativement pauvre. Disons qu'elle est ridicule à côté de celles de Java, .Net, Python, Perl, Ruby… Par exemple, il n'y a pas de bibliothèque haut-niveau pour faire du réseau, la gestion des matrices est quasi-absente, les fonctions de manipulation de chaines de caractères sont trop peu nombreuses. Par exemple, il y a String.iter, mais pas String.map. Il y a Array.init, mais pas List.init. Avec le type Map (arbre binaire associatif), on a une fonction iter pour parcourir les éléments en faisant des effets de bord, mais il n'y a même pas de fonction convertissant en liste. Ce sont pourtant des fonctions de base. Il existe bien sûr des bibliothèques annexes, plus ou moins maintenues, mais elles ne sont pas standard.

Strings

OCaml se veut un langage fonctionnel, décourageant les effets de bord. Pourtant, c'est l'un des rares langages où les strings sont modifiables. Même dans des langages de script comme Python, elles sont constantes. Même en C, il est possible d'avoir des chaînes constantes (via le mot-clé const). En Caml, rien. En bricolant une bibliothèque, on peut retrouver ce comportement, mais on entre alors dans le domaine de la bidouille.

Il existe plusieurs “constantes” dans la bibliothèque : Sys.os_type, Sys.executable_name, Sys.ocaml_version, etc. Contrairement à ce que l'on s'attend, on peut les modifier, ce qui donne des comportements surprenants et peu sûrs :

# let s = Sys.ocaml_version;;
val s : string = "3.10.2"
# s.[2] <- '4';;
- : unit = ()
# Sys.ocaml_version;;
- : string = "3.40.2"

Par conséquent, à chaque fois que l'on passe une chaîne de caractères à une fonction externe dont on ne maîtrise pas le code, on ne peut rien supposer quant à la valeur de la string au retour de la fonction. Nous voilà donc dans un langage aussi sûr que du C sans const.

Alors que Caml fait la guerre aux valeurs non initialisées, en imposant de donner une valeur à chaque nouvelle définition (ce qui est parfois lourd quand on utilise les traits impératifs), le langage fournit la fonction String.create. Cela crée une string, de la longueur de notre choix et le manuel précise : “The string initially contains arbitrary characters.” Pour faire simple, c'est une valeur non initialisée, c'est-à-dire un manque de cohérence avec le reste. Etrangement, la fonction symétrique Array.create prend un argument supplémentaire d'initialisation. Pourquoi deux choix différents ?

Il est fréquent en programmation de vouloir construire des chaînes dynamiquement. Les développeurs Java/.NET compétents vous diront d'utiliser le type StringBuilder si l'on fait de nombreuses concaténations, pour des raisons de performance. Alors que le type String de Caml autorise la modification de caractères, il est interdit l'ajout de caractères. Le développeur moyen choisira alors d'utiliser l'opérateur de concaténation, ce qui peut être ruineux pour les performances (l'alternative impérative donne un code plus verbeux et plus lourd à maintenir).

Mais de toute façon, on ne peut pas non plus ajouter d'élément à un tableau (même le C a la fonction “realloc”)… Ou plutôt, il faut chercher une bibliothèque externe.

Surcharge

OCaml est l'un des rares langages à ne pas avoir de surcharge. Même le C possède une forme de surcharge pour ses opérateurs de base. Le domaine où la surcharge manque le plus est l'arithmétique. Par exemple, si l'on souhaite faire des calculs sur des int64 :

let x = Int64.add 38L 4L
let f y = Int64.div (Int64.mul x (Int64.of_int y)) 10L

Oui, j'ai dû écrire quatre fois Int64 en deux lignes… admirez l'inférence de type !

Il est bien sûr possible de redéfinir les opérateurs ou d'en créer, mais on tombe vite à court d'imagination pour les noms d'opérateurs, si l'on souhaite mélanger plusieurs types dans un programme. On trouve des astuces, des bidouilles possibles dans certains cas, mais ça alourdit souvent le code.

Au final, on se retrouve à indiquer le type dans le nom des fonctions utilisées : print_int, print_string, print_char, etc. Dans le cas de l'affichage, on remplace souvent tout cela par un printf, mais il s'agit d'un cas très particulier (d'ailleurs, le printf est un cas spécial dans le compilateur). On peut alors se demander si l'on a vraiment de l'inférence, vu que l'on se retrouve à écrire le type un peu partout.

Open

La commande open permet d'importer un module. Il importe toutes les déclarations qui sont faites et masque celles qui existaient. Ce masquage peut être dangereux. Plus d'un développeur utilise “open List” dans son code. Si un jour les concepteurs de Caml décident d'ajouter la fonction min dans le module List (et ce serait naturel d'avoir List.min), cette fonction remplacerait silencieusement la fonction min de Pervasives.

Ce problème arrive aussi et surtout pour les modules de l'utilisateur. Que se passe-t-il si deux modules appelés avec open utilisent un même identifiant ? Le résultat dépend de l'ordre d'ouverture des modules, ce qui peut avoir des effets inattendus, pour peu qu'ils aient le même type.

Généricité

Comment faire une fonction travaillant sur n'importe quel type de collection (liste, tableau…) ? La plupart des langages depuis C++ possèdent des itérateurs. Pas OCaml (sauf à utiliser des bibliothèques non standards).

Comment faire une fonction travaillant sur des nombres de n'importe quels types (int, int64…) ? La plupart des langages permettent cela par le biais d'un template, d'une interface ou du typage dynamique. OCaml n'a rien.

Comment afficher une valeur, quel que soit son type ? C++ possède l'opérateur <<, Java et C# possèdent une méthode ToString, les autres langages ont leur solution. Pas OCaml (sauf en bidouillant vraiment en profondeur et en utilisant des extensions non standard).

Ainsi, les développeurs Caml :

  • n'écrivent pas de code générique (“Que l'utilisateur de ma bibliothèque utilise des listes, tout comme moi !”) ;
  • dupliquent leur code ;
  • utilisent des fonctions d'ordre supérieur et passent en argument les fonctions nécessaires à la fonction. Par exemple, une fonction peut demander à l'utilisateur de fournir l'opérateur d'addition qui va bien, ou la fonction iter. Ce qui alourdit la code et nuit à la lecture.

Fonctions à plusieurs arguments

Il est facile d'écrire une fonction qui renvoie l'opposé de la fonction donnée en argument :

let notf f x = not (f x)

C'est facile… sauf que ça ne marche que pour les fonctions à un argument. D'une façon générale, dès que l'on souhaite manipuler et transformer des fonctions, il faut passer par une fonction à un seul argument (un tuple). Pourtant, il est possible de trouver le type à la main : ('a → bool) → ('a → bool), puisque 'a peut désigner n'importe quelle valeur. Mais Caml le refuse.

Autre exemple du même genre, où on souhaite appeler une fonction en utilisant une valeur par défaut en cas d'exception :

let trydef def f x =
  try f x
  with _ -> def

Introspection

Cette fonctionnalité n'existe pas. Tout est statique.

On peut avoir un semblant d'introspection avec le module Obj, mais c'est très limité, c'est dangereux et, d'ailleurs, Xavier Leroy avertit :

Since then, I've been grepping for “Obj.magic” in every bug report that we get, and throwing away immediately those where it occurs. You've been warned!

Ainsi que :

Be warned, however, that the functions provided by the Obj module thoroughly break type safety in every possible way, and can even invalidate some optimizations in the ocamlopt compiler. You should really not use it – and this is why this module has no existence in the documentation.

Sérialisation

La sérialisation n'est pas sûre en Caml, ce qui peut provoquer des erreurs de segmentation dans certains cas. La documentation l'indique d'ailleurs : “Anything can happen at run-time if the object in the file does not belong to the given type”. C'est rassurant !

Limitations du typage

Les codes suivants, que l'on pourrait imaginer valides, sont refusés par le système de typage de Caml :

let foo x = x 1, x "a"

Pourtant, x pourrait être la fonction identité (il y a d'autres possibilités).

let f () = Printf.printf "foo"
let g () = 42
let list = [f; g] in List.iter (fun f -> ignore (f ())) list

Caml interdit en effet les listes hétérogènes, même si c'est entièrement sûr (et vérifiable à la compilation).

let rec f x = x
and g() = f 1
and h() = f "a"

Le type de f n'est pas généralisé, alors qu'il pourrait l'être.

De même, le système de typage refuse le code suivant :

type t = MyInt of int | MyFloat of float | MyString of string
 
let foo printerf = function
 | MyInt i -> printerf string_of_int i
 | MyFloat x -> printerf string_of_float x
 | MyString s -> printerf (fun x -> x) s

Opérateurs de comparaison

À cause de l'absence de surcharge, on se retrouve avec (+), (+.), (+/), Int64.add… pour les additions ; (-), (-.), (-/), Int64.sub… pour les soustractions ; print_int, print_string, print_char, print_float… pour l'affichage. Et pour la comparaison entre les types ? Il y a des opérateurs génériques. C'est surprenant, se sont-ils rendus compte à ce moment-là que cela devenait ingérable ?

La première conséquence, c'est que le système de typage accepte de comparer n'importe quelle valeur avec une autre du même type. D'accord, mais comment compare-t-on des fonctions ? La réponse est simple : on lance une exception à l'exécution.

La deuxième conséquence, c'est que les types utilisent une fonction de comparaison par défaut. Pour les types de base, le comportement est cohérent. Pour les types plus complexes, le comportement est rarement ce lui que l'on souhaite. Pire : il n'est pas possible de choisir le comportement.

type value =  Valeur of int
            | Valet
            | Dame
            | Roi
            | As

Il n'est pas possible d'utiliser les opérateurs de comparaison sur ce type. Ou plutôt : ils renvoient une valeur dépendante de l'implémentation. En pratique, ils ne renvoient pas la valeur que l'on imagine/souhaite (ici : Valet < Dame, mais Valet < Valeur 4).

La seule solution est de définir ses propres fonctions de comparaison : il faut trouver des nouveaux noms pour <, <=, =, <>, >, >=, compare, min et max. Il faut donc avoir pas mal d'imagination, ou alors se limiter à quelques fonctions. Par ailleurs, il faudra être vigilant tout au long du code et ne jamais appeler les fonctions standard. Le système de typage ne vous sera d'aucune aide et la moindre erreur mènera à un résultat inattendu.

Support de Windows

OCaml est un outil développé sous Unix et pour Unix. Ses développeurs reconnaissent ne pas aimer Windows, ce qui fait que Caml est très mal géré. D'ailleurs, Xavier Leroy a même avoué :

You may have noticed I did not release Windows binaries for OCaml 3.11.1 and 3.11.2 We have been lazy and it's a bit of an experiment. I'm waiting to see how many people are complaining and hoping someone will complain so loudly that I can tell: “Do it yourself!” So far, the cunning plan hasn't worked.

Un langage à l'abandon

Plus personne n'est payé à temps plein pour travailler sur OCaml depuis des années. Les quelques contributeurs passent plus de temps sur d'autres projets. En tout, il y a l'équivalent d'à peine une personne sur le projet.

ocaml.txt · Dernière modification: 2013/05/06 13:23 (modification externe)