Sur le choix d'OCaml

J'ai commencé à utiliser régulièrement le langage OCaml vers 2012, et depuis, mon intérêt et mon engouement pour ce langage n'ont cessé de croître. Il est devenu mon choix de prédilection pour presque tous mes projets personnels, influençant également mes choix professionnels. Depuis 2014, je participe activement aux conférences grand public dédiées à la programmation et à la construction de logiciels, où j'exprime souvent mon enthousiasme pour OCaml de manière parfois excessive (mais toujours passionnée). Cela m'a valu, de manière amicale, le surnom d'évangéliste d'OCaml, une appellation qui, je l'avoue, me flatte énormément. Convaincu que mon intérêt pour OCaml est justifié, je prends souvent plaisir à énumérer les nombreux avantages de cette technologie. J'ai donc décidé de coucher par écrit les raisons pour lesquelles je considère OCaml comme un excellent choix pour divers types de projets. Cette page me permettra de partager facilement mes arguments et d'expliquer pourquoi OCaml mérite attention et intérêt. De plus, je ne suis pas seul à penser cela. Malgré l'idée reçue que OCaml ne serait pas un choix pragmatique pour l'industrie, de grandes entreprises telles que Meta, Microsoft, Ahref, Tarides, OCamlPro, Bloomberg, Docker, Janestreet, Citrix, Tezos, et bien d'autres l'utilisent activement.

Dans ce billet d'opinion, je vais essayer de présenter brièvement ma rencontre avec le langage, et d'énumérer ses avantages — répartis en plusieurs rubriques portant sur le langage lui-même, son écosystème et sa communauté. Je tâcherai également de débunker certains mythes (ou idées reçues) populaires sur Internet. Par souci de transparence, il est important de préciser qu'à l'heure où j'écris ces lignes, mon activité professionnelle consiste à travailler pour et sur l'écosystème OCaml. Cependant, les lecteurs qui me suivent depuis plusieurs années pourront témoigner que je faisais la promotion du langage bien avant d'être rémunéré pour travailler sur l'écosystème OCaml, parfois de manière immodérée.

Avant-propos

Premièrement, cet article expliquera pourquoi je trouve personnellement qu'OCaml est un choix pertinent dans de nombreux contextes. Mon but n'est pas particulièrement de vous convaincre — même si cela serait un effet de bord tout à fait bénéfique — et il est fort probable que beaucoup des arguments que je présenterai s'appliquent aussi à d'autres langages !

Ensuite, très souvent, quand je propose OCaml à des gens qui souhaitent découvrir de nouveaux langages ou encore des solutions écrites en OCaml, on me fait gentiment remarquer que je fais toujours la promotion d'OCaml. Il est amusant de constater que lorsque les propositions concernent des langages adoptés _par défaut comme JavaScript ou des langages plus récents comme Rust ou Go, cela suscite souvent moins de réactions. Probablement parce que l'on pense implicitement que la proposition d'un langage moins connu tend vers l'irrationalité et les préférences personnelles. De mon point de vue, proposer OCaml est, dans beaucoup de cas où le contrôle mémoire fin n'est pas nécessaire, aussi pertinent que proposer Rust (et probablement plus).

Pour terminer cet avant-propos, beaucoup de personnes ont été confrontées à OCaml (ou Caml Light) en licence ou en classes préparatoires, l'utilisant dans des contextes souvent très éloignés de l'industrie. Pour ma part, j'ai commencé à m'intéresser à OCaml bien avant, grâce au Site du Zéro, où une petite communauté de programmeurs férus de fonctionnel faisait la promotion de langages moins mainstream comme OCaml, Erlang et Haskell. Mon interaction avec OCaml à l'université n'était que du bonus.

Autres ressources

Je ne suis pas le premier à avoir documenté les raisons qui poussent à choisir OCaml. Il existe de nombreuses alternatives qui, selon moi, valent aussi le coup d'être consultées et qui démontrent que les utilisateurs d'OCaml en sont généralement très satisfaits, les motivant à communiquer sur comment et pourquoi nous avons fait le choix du langage comme technologie principale :

Il existe probablement d'autres ressources et témoignages, notamment sur le site officiel qui propose des témoignages industriels et académiques. De même qu'il existe des articles témoignant du dépit que peut provoquer OCaml. Je suis conscient qu'OCaml n'est pas parfait — je pense qu'il n'existe pas de technologie parfaite. Il est probable que j'évoque (implicitement ou explicitement) certains de ces articles dans la section dédiée aux mythes et en conclusion, où je tâcherai d'expliquer dans quels contextes je ne trouve pas l'utilisation d'OCaml pertinente.

OCaml en tant que langage

Avant de rentrer dans les fonctionnalités offertes par le langage, j'aimerais commencer par un point qui, selon moi, est essentiel. OCaml est un langage de programmation issu de la recherche, utilisé par des utilisateurs industriels. Cette dualité est importante car elle offre au langage deux choses :

Cet entrelacement entre des motivations industrielles et académiques permet à OCaml de proposer une collection de fonctionnalités solides, utiles et clairement définies. En d'autres termes, OCaml est un langage vivant et depuis que je m'en sers, j'ai eu l'occasion d'être témoin de nombreuses évolutions et ajouts très — de mon point de vue — désirables et qui débunkent une assertion très courante en défaveur de OCaml : le langage ne sert que pour de la théorie ou pour l'implémentation de Coq/Rocq. Même si, historiquement, c'était vrai, les motivations apportées par les utilisateurs industriels justifient le titre "An industrial-strength functional programming language with an emphasis on expressiveness and safety". La Keynote d'ouverture du OCaml Workshop 2021 de Xavier Leroy, "25 Years Of OCaml" présente une frise chronologique exhaustive de la conception continue de OCaml, montrant les différentes phases d'évolutions par lesquelles est passé le langage.

Dans les grandes lignes, OCaml est un langage de programmation de la famille ML, haut-niveau (ici, à lire comme étant doté d'un glâneur de cellules), statiquement typé (dont les types sont vérifiés à la compilation et ne font pas de conversions implicites), avec de la synthèse de types (aussi appelée inférence de types, laissant la main au compilateur, dans une majorité de cas, de déduire le type d'une expression) permettant de programmer dans un style fonctionnel et impératif. Il dispose aussi d'un modèle de programmation par objets et d'un langage de modules très riche. OCaml dispose de deux schémas de compilation : ocamlc, qui permet de compiler vers un bytecode interprétable par une machine virtuelle (portable et efficace), et ocamlopt, qui compile vers du code machine (exécutable dans une grande diversité d'architectures). De plus, OCaml permet la conversion de son bytecode en JavaScript au moyen de Js_of_ocaml, permettant une interopérabilité très rapide avec l'écosystème OCaml (je l'utilise d'ailleurs massivement sur ce site web). La même approche est utilisée pour produire du WebAssembly. Pour une interopérabilité plus poussée avec l'écosystème JavaScript, Melange utilise une approche sensiblement différente de Js_of_ocaml pour produire du JavaScript robuste.

OCaml est un langage très versatile et maintenant, je vais tâcher de présenter les fonctionnalités et apports du langage qui en font — pour moi — un outil idéal pour la construction de projets personnels et professionnels, en commençant par un petit détour sur le typage statique.

Sur la vérification statique des types

Quand je préparais, avec Bruno, l'épisode If This Then Dev dédié à OCaml, qui, au final, a été enregistré avec Didier, il m'a posé une question que j'ai trouvée surprenante :

Est-ce que ça vaut vraiment la peine de s'embêter avec des types quand on fait un projet perso, rapidement ? Même si, pour de la prod, je vois parfaitement l'intérêt, pour un projet personnel, ça me semble être une perte de temps.

Je pense qu'il y a deux axes de réponse. Le premier, et le plus évident, c'est que, dans l'absolu, je ne vois pas pourquoi un projet personnel devrait être moins hygiénique qu'un projet professionnel. Quand j'écris un logiciel pour moi, je peux effectivement me contenter de ne pas le faire se heurter aux corner cases de mon implémentation, certes. Mais ce n'est probablement pas ce que j'ai envie de faire. Donc, si un langage et son compilateur me permettent de tendre des filets de sécurité pour me forcer à prendre en compte tous les cas d'un logiciel, je prends — de la même manière que rédiger des tests unitaires facilite mon développement et je ne les vois pas comme une contrainte.

Mais au-delà des considérations sur l'hygiène que l'on souhaite apporter à un projet personnel, je pense que, généralement, la mauvaise presse de la vérification statique des types est souvent la conséquence d'une mauvaise expérience. En effet, dans des langages comme C, ou encore Java, les types sont essentiellement une contrainte qu'il est possible de dérouter facilement. Dans des langages qui portent un intérêt fort au typage, comme OCaml, Haskell, F#, Scala ou encore Rust, les types servent de garde-fous, mais, et selon moi, c'est le point le plus important, les types servent aussi d'outil de design expressif. En les utilisant, on gagne en sécurité, mais on dispose également d'un outil de description de données incroyablement riche, versatile et concis.

D'après mon expérience, même s'il est courant de passer d'un langage mal-typé (désolé, la tentation est trop forte) à un langage dynamiquement typé — j'ai, par exemple, expérimenté le passage de Java à Ruby avec beaucoup de joie — passer d'un langage avec un système de type riche, comme OCaml ou Haskell, rend le passage à un langage dynamiquement typé largement plus compliqué. À l'heure actuelle, je ne connais personne ayant expérimenté sérieusement des langages comme OCaml ou Haskell, qui soit ravi de revenir à des langages aux systèmes de types moins sophistiqués (même si la motivation d'un projet intéressant peut justifier la régression technologique).

Ce n'est d'ailleurs pas une observation personnelle ; la vérification statique des types fait partie intégrante du grand débat sur l'évolution des langages. Des langages historiques muent (ou tentent de muer) pour intégrer plus de vérification des types. Par exemple, Erlang, dès les années 80 (avant la libération du code source de son compilateur), avait expérimenté l'intégration d'un système de type. Et Java améliore, de version en version, les fonctionnalités destinées à améliorer la vérification statique des types, en intégrant, par exemple, des familles scellées. Beaucoup de langages expérimentent l'intégration de systèmes de types : Ruby avec RBS (ou encore Crystal, un langage statiquement typé, drastiquement inspiré par Ruby), Python et Mypy, Elixir, qui revient sur les tentatives passées d'Erlang en proposant une approche graduelle viable, et évidemment, TypeScript qui est devenu largement adopté par la communauté des développeurs et développeuses JavaScript. Même si toutes ces initiatives sont très motivantes, et vont clairement, selon moi, dans la bonne direction, actuellement, ces propositions ajoutent des garde-fous, mais ne servent pas encore d'outils de design expressifs.

Dans l'utilisation de systèmes de types de plus en plus riches, la Maison Blanche a récemment publié un communiqué qui insiste sur l'importance de la memory safety dans la conception de programmes et ... plébiscite l'utilisation du langage Rust (historiquement écrit en OCaml, avant d'être auto-hébergé) au détriment de C++, montrant très explicitement que même les instances officielles (et souvent présentées comme poussiéreuses) soulignent l'importance des systèmes de types riches — d'ailleurs, la réponse formulée par Tarides, l'entreprise où je travaille à l'heure où je rédige cet article, est aussi porteuse d'arguments intéressants en faveur de l'utilisation d'OCaml pour la construction de systèmes critiques.

Pour conclure, la vérification statique des types, c'est vraiment bien et recommandé, et ça vaut le coup de regarder des langages disposant de systèmes de types sophistiqués (comme OCaml) et pourquoi pas, d'aller encore plus loin, en s'intéressant de plus en plus aux méthodes formelles.

Fonctionnalités du langage

Même s'il est très tentant de faire un gigantesque tutoriel sur OCaml, l'objectif de cet article est de présenter, dans cette section, ce qui fait que pour moi, OCaml est un choix très pertinent pour l'apprentissage et la production. Les avantages seront donc présentés (et défendus), mais ce n'est pas un tutoriel.

Un langage multi-paradigmes

De nos jours, parler de langages multi-paradigmes peut sembler peu sensé car une très large partie des langages de programmation plébiscités par l'industrie sont déjà multi-paradigmes. Cependant, OCaml est un langage de programmation fonctionnelle, permettant la programmation impérative, la programmation modulaire, la programmation par objets, et depuis la version 5.0.0 du langage, la programmation multi-cœur. Comme Haskell a pignon sur rue quand on parle de programmation fonctionnelle, il arrive souvent que l'on considère que proposer des mécanismes impératifs, dans un langage, est une mauvaise idée, surtout si l'on est convaincu des bienfaits du style fonctionnel. De mon point de vue, il existe plusieurs raisons parfaitement légitimes de faire de la programmation impérative, si le langage le permet :

De manière générale, la nature à la fois impérative et fonctionnelle d'OCaml permet de tirer parti des différents avantages des deux paradigmes dans des situations différentes et, évidemment, de les coupler. Par exemple, en cachant la nature impérative d'un module sous une API fonctionnelle.

Syntaxe à la ML

Bien que la syntaxe soit souvent considérée comme un détail, les langages de la famille ML disposent d'une syntaxe concise, expressive et lisible. Même si cette famille de syntaxe peut être déroutante quand on vient de syntaxe plus conventionnelle, inspirée de C, on s'y fait assez rapidement et l'on peut très vite se rendre compte qu'elle est très cohérente et relativement peu ambigüe. Cependant, si la syntaxe de OCaml vous pose des soucis, n'hésitez pas à vous tourner vers ReasonML, une syntaxe alternative — avec des accolades.

Étroitement lié à la recherche

OCaml est un langage issu de la recherche française, comme en témoigne l'histoire de Caml, essentiellement pour permettre l'implémentation de l'assistant de preuves Coq/Rocq. Cette provenance — et les motivations initiales, implémenter Coq, mais aussi servir de langage de programmation enseigné en classes préparatoires — induit une certaine dualité :

Cette rigueur théorique, engendrée par une proximité indéniable avec le monde de la recherche, fait que les différents aspects de OCaml sont bien documentés, illustrés par un grand nombre de publications et possèdent des comportements prévisibles. Ce qui fait que, de mon point de vue, OCaml est un choix très judicieux pour comprendre, en profondeur, ces différentes fonctionnalités. Par exemple, je pense que OCaml m'a permis de largement mieux comprendre certains traits ou paradigmes de langages.

D'ailleurs, un très bon exemple démontrant comment un travail méticuleux et rigoureux de recherche peut servir l'intégration d'un aspect de langage est l'implémentation d'un modèle objet dans OCaml. En effet, la thèse de Jérôme Vouillon, Conception et réalisation d'une extension du langage ML avec des objets propose un modèle objet novateur, qui se marie très bien à l'inférence de types en séparant la notion d'héritage et de sous-typage — l'héritage étant une notion syntaxique et le sous-typage étant une notion sémantique — utilisant du polymorphisme de rangée pour décrire des relations de sous-typage structurelles, par opposition au sous-typage nominal, utilisé par Java, C#, et une grande partie des langages de programmation OOP populaires. Le modèle objet de OCaml se conforme parfaitement au principe SOLID sans cérémonie additionnelle.

Types algébriques

J'ai été assez expansif sur les raisons qui font que je trouve qu'un langage soit doté d'une analyse statique des types. Cependant, dans mon expérience, je trouve que pour qu'un langage statiquement typé soit réellement utilisable, la présence de types algébriques est nécessaire :

Couplé avec de la correspondance de motifs et du polymorphisme paramétrique (ou génériques), un système de types algébriques est un outil formidablement expressif pour décrire des structures de données, la machine à état d'un programme, ou l'expression d'un domaine métier avec une cardinalité adaptée. Même s'il est, au 21ème siècle, courant d'avoir des produits et des exponentiels, quand je suis amené à utiliser des langages très populaires, je suis souvent frustré de l'absence de sommes, m'obligeant à utiliser un encodage verbeux (et augmentant la cardinalité d'un domaine). C'est très flagrant dans l'utilisation de Go et de TypeScript.

L'intérêt de cette triade est d'ailleurs, probablement, une des raisons (couplée à un écosystème et une chaîne d'outillage très ergonomique) qui explique le succès de Rust. En bref, si vous avez l'intention de construire un nouveau langage de programmation, doté d'une vérification statique des types, par pitié, n'hésitez surtout pas à intégrer des types algébriques !

Pour terminer, il existe des pans du systèmes de types de OCaml que je n'ai pas couverts, mais qui méritent probablement des articles dédiés. Par exemple, les types algébriques généralisés qui permettent de décrire encore plus d'invariants.

Programmation modulaires et langage de modules

OCaml, par le biais de Caml Light, son ancêtre, figure parmi les premiers langages à proposer un système de modules, à l'instar de Standard ML, offrant ainsi l'encapsulation et l'abstraction tout en permettant la compilation séparée, à la manière de Modula-2. Le langage de modules d'OCaml constitue un aspect fondamental de ce langage, bien que sa complexité puisse intimider. En effet, en OCaml, il est possible de distinguer clairement l'interface (la signature) de l'implémentation (la structure), facilitant ainsi l'encapsulation et la documentation, tout en autorisant l'application de fonctions dans le langage des modules.

Il m'est particulièrement difficile d'aborder brièvement le sujet des modules (c'est un domaine que j'aspire à explorer depuis des années sur mon blog). Cependant, voici une liste des avantages que je perçois dans cette approche très modulaire d'OCaml :

La théorie derrière les langages de modules dans les langages ML est un sujet très vaste, toujours en évolution, qu'il est très difficile de résumer dans un paragraphe. Cependant, l'introduction de la thèse de Derek Dreyer, Understanding and Evolving the ML Module System donne une très bonne explication sur les intérêts des modules, de leurs usages, illustrés avec beaucoup d'exemples. J'espère cependant prendre du temps dans les semaines/mois à venir pour écrire sur le langage de modules, plus expansivement que ce que j'ai déjà tenté, parce que ça pourrait être formateur et que le domaine est, je trouve, très très intéressant !

Injection et inversion de dépendances

En parlant brièvement de la programmation orientée objets en OCaml, j'ai évoqué rapidement le fait que OCaml permet d'exprimer, par le biais des fonctionnalités offertes par le langage, trivialement, des prérequis pour écrire du code SOLID. Le dernier point que j'aimerais souligner est la facilité d'inversion des dépendances à injecter au moyen de fonctionnalités offertes par le langage. Dans les grandes lignes, le principe d'inversion des dépendances consiste à décrire des treillis de dépendance au moyen d'abstractions et non d'implémentations. De cette manière, les dépendances peuvent-être injectées à postériori — rendant, notamment, le changement de contextes, pour des tests unitaires par exemple, trivialement implémentables.

OCaml dispose de deux outils facilitant cette inversion, et pouvant être utiles dans des contextes différents. Et nous allons nous inspirer de l'exemples très populaires du télétype pour montrer comment inverser les dépendances :

let program () =
  let () = print_endline "Hello World" in
  let () = print_endline "What is your name?" in
  let name = read_line () in
  print_endline ("Hello " ^ name)

Même si ça peut ne pas sembler évident, ce programme dépend d'implémentations concrètes, les interactions avec l'entrée et la sortie standards.

Par le biais de modules

L'approche la plus évidente consiste à utiliser des modules, comme valeurs de premier ordre ou par construction, au moyen de foncteurs. La dualité entre les signatures et les structures rend l'inversion de dépendances évidente. Par exemple, pour reprendre notre exemple, voici comment, en utilisant des first-class modules, il est très facile de dépendre d'un ensemble d'interaction abstrait. On commence par décrire la représentation abstraite des interactions possibles :

module type IO = sig
  val print_endline : string -> unit
  val read_line : unit -> string
end

On peut maintenant attendre de notre fonction program qu'elle prenne un module du type IO en argument (on appellera ça un gestionnaire) et utiliser les fonctions exportées par le module, qui, dans notre exemple, s'appelle Handler :

let program (module Handler: IO) =
  let () = Handler.print_endline "Hello World" in
  let () = Handler.print_endline "What is your name?" in
  let name = Handler.read_line () in
  Handler.print_endline ("Hello " ^ name)

Et il est possible, par exemple, dans le contexte des tests unitaires, de fournir une implémentation qui journalise l'ensemble des opérations appelées (et qui mock l'appel de read_line, pour fixer le résultat renvoyé). Cela rend l'expression de tests unitaires testant la logique métier très facile à implémenter.

L'action de passer une implémentation concrète en argument à notre fonction consiste à interpréter le programme.

Par le biais d'effets définis par l'utilisateur

La version 5 d'OCaml est arrivée avec son lot de nouveautés. Cependant, la plus grande avancée est la refonte du runtime d'OCaml pour prendre en charge le multi-cœur. Il existe plusieurs manières de décrire des algorithmes concurrents, par exemple en utilisant des acteurs ou encore des canaux. OCaml a fait le choix d'utiliser des effets, permettant de simplifier le traitement du flot de contrôle du programme. En effet OCaml permet à l'utilisateur de décrire ses propres effets, que l'on appelle logiquement, des effets définis par l'utilisateur. Même s'ils forment un outil formidable pour décrire des programme concurrents, ils permettent aussi de faciliter l'injection de dépendances quand on veut garder la main, au niveau du gestionnaire, sur le flot d'exécution d'un programme.

Attention, dans mon exemple, j'utilise une syntaxe expérimentale, tout juste fusionnée dans le tronc de OCaml, et qui sera probablement disponible dans la version 5.3.0 du langage.

Comme pour notre amélioration précédente, il faut d'abord décrire l'ensemble des opérations que l'on pourra produire. On utilise la construction effect :

effect Print_endline : string -> unit
effect Read_line : unit -> string

Ensuite, on peut écrire, dans un style directe, notre programme en produisant des effets :

let program () =
  let () = Effect.perform (Print_endline "Hello World") in
  let () = Effect.perform (Print_endline "What is your name?") in
  let name = Effect.perform (Read_line ()) in
  Effect.perform (Print_endline ("Hello " ^ name))

Il est ensuite possible d'interpréter, à postériori, notre programme en utilisant une construction similaire au filtrage par motif pour donner un sens spécifique à chaque effet.

Actuellement, on regrettera que la propagation d'effets ne soit pas capturée par le système de types. Cependant, il s'agit d'une fonctionnalité expérimentale, que l'on utilise massivement dans la nouvelle version de YOCaml. Je sais que des ressources sont allouées à l'élaboration d'un système de type efficace pour tracker la propagation d'effets !

En général, quand je ne me soucie pas du contrôle du flot du programme, ou que je ne veux pas pouvoir ajouter des effets à postériori, j'utilise des modules. Mais dans le cas de YOCaml, on a profité de l'utilisation du nouveau système d'effets pour introduire des effets dédiés aux tests unitaires, permettant, par exemple, de mocker le temps qui passe.

Une fois de plus, il est vraiment très compliqué de ne pas trop s'épancher sur les effets définis par l'utilisateur, qui est une toute nouvelle fonctionnalité très excitante du langage. Je terminerai en me contentant de vous partager deux articles écrits par Arthur Wendling expliquant, très pédagogiquement, l'utilisation des effets, ainsi qu'une bibliographie très complète de la littérature relative à l'abstraction d'effets en programmation fonctionnelle :

À noter qu'il serait aussi possible de procéder à cette inversion/injection au moyen de records ou d'objets, cependant, mon expérience en OCaml m'indique que les approches avec des modules ou des effets (quand on veut pouvoir manipuler le flot de contrôle), sont souvent plus directes et facile à raisonner.

Concernant le futur

OCaml est un langage toujours en activité qui, de version en version, évolue. Dans la section dédiée à l'inversion de dépendances, j'ai rapidement parlé de l'inclusion toute récente d'effets dans le langage pour décrire un runtime multi-cœur, témoignant des mutations dont bénéficie le langage au fil des années. On notera aussi l'intégration des opérateurs de liaisons, rendant l'utilisation de la triade Foncteurs, Foncteurs Applicatifs et Monades plus confortable — à la manière des expressions de calcul, en F#.

Actuellement, beaucoup de chantiers très excitants sont en œuvre pour améliorer, encore plus, le langage :

On notera aussi le développement d'un système de macro hygiénique, de l'intégration progressive d'un système de métaprogrammation à étages, de l'implémentation d'un back-end d'optimisation, témoignant de l'activité forte de OCaml dans le secteur de l'innovation et rendant son développement, pour les années à venir, très motivant et excitant !

Points faibles

Même si je suis convaincu que OCaml est un excellent langage, dire qu'il est parfait serait probablement de la très mauvaise foi — en effet, rien n'est malheureusement parfait. Voici, selon moi, quelques points ombrageant OCaml en tant que langage :

Je pense que ces points faibles sont globalement discutables (parce qu'ils sont souvent justifiés), mais je comprend parfaitement qu'ils peuvent être perturbants. Cependant, je pense qu'ils ne suffisent pas à rendre OCaml inutilisable et qu'ils ne devraient pas être une barrière trop grande pour se mettre à OCaml ! Et le bénéfice d'avoir un langage améliorable, c'est qu'il reste, en permanence, une volée d'améliorations potentielles, motivant des travaux qui peuvent, en plus, bénéficier à d'autres langages. Et, en toute sincérité, en étant conscient de ces parties rugueuses, j'ai plus souvent eu l'occasion de râler de l'absence de traits de langages, présents dans OCaml, dans d'autres langages, que de râler de ces parties en écrivant du OCaml, pour lesquelles il existe, souvent, des solutions (parfois, à la limite de la satisfaction, je vous l'accorde) permettant de travailler sereinement.

Pour conclure sur le langage

J'ai, dans les très très grandes lignes, survolé des raisons qui font que, selon moi, apprendre OCaml est un choix très pertinent. Ce langage permet de comprendre fondamentalement certains idiomes de programmation très populaires (mais souvent mal définis). De plus, certains aspects du langage servent parfaitement des desseins industriels rendant, parfois, des bonnes pratiques, triviales à exprimer ! Une grande partie de ces attraits est expérimentable avec d'autres langages, cependant, la nature fortement multi-paradigmes de OCaml permet de centraliser son apprentissage dans un seul langage. À ma connaissance, dans la jungle de langages partiellement populaires, seul Scala semble couvrir autant de sujets, même si, de mon point de vue, son modèle objet est, essentiellement par soucis d'interopérabilité avec les autres langages de la JVM, largement moins intéressant.

Comme l'objectif de cet article n'est pas d'être un tutoriel, je suis volontairement passé rapidement sur certains concepts, les modules et les effets. Je n'ai presque pas parlé des objets, des variants polymorphes et des types algébriques généralisés. Si jamais ces sujets vous intéressent, je vous invite à lire en détail l'excellent Using, Understanding, and Unraveling The OCaml Language, de Didier Remy, couplé aux livres que j'ai présentés en introduction, qui est une mine d'or pour toute personne désireuse d'approfondir ses connaissances en OCaml.

Pour conclure, OCaml offre un outillage, au niveau du langage, varié et riche pour l'apprentissage de la programmation, la construction de programmes industriels respectant des standards mais aussi l'implémentation structure de données complexes et d'abstractions issu de la théorie des catégories comme un noyau fonctionnel, des traits impératifs, un système de types inférés riche et expressif (permettant l'expression de types algébriques, facilitant l'expression de domaines clairs), un langage de modules comme outil d'abstraction, de réutilisabilité et de définition d'unités de compilation, un modèle objet, la possibilité d'exprimer des effets que l'on peut propager et interpréter à postériori et d'autres fonctionnalités avancées. Ne serait-ce que pour appréhender des concepts avancés de programmation, OCaml est un excellent candidat — c'est d'ailleurs pour ça que OCaml est une inspiration évidente pour beaucoup de langages plus récents, Rust étant un des exemples notables.

OCaml en tant qu'écosystème

Avoir un langage expressif est très bénéfique pour construire des choses (la formulation est volontairement naïve). Cependant, dans différents contextes, le professionnel et le personnel, ce n'est pas suffisant :

C'est pour ça que les fonctionnalités offertes par le langage ne sont pas une métrique suffisante pour décrire sa viabilité pour construire et maintenir des projets. L'écosystème est aussi un point très important. C'est d'ailleurs pour ces raisons que .NET et la JVM, par le biais de langages relativement peu expressifs (mais en progrès) comme Java et C# sont aussi populaires. Pour juger la pertinence d'un écosystème, je pense qu'il est important de prendre en compte plusieurs critères :

Dans cette section, nous allons essayer de survoler ces différents points pour voir si l'écosystème OCaml est à la hauteur du langage. Je tiens à préciser que je suis un peu biaisé parce que je suis convaincu de la pertinence de OCaml depuis 2012, à l'époque où l'écosystème était drastiquement plus pauvre. A cette époque, j'ai essayé de construire des projets en composant avec les manques ce qui a engendré, probablement, un biais du survivant. Comme de nos jours, notamment grâce à des utilisateurs industriels, l'écosystème de OCaml est largement plus riche et étendu, il est cependant devenu beaucoup plus simple de le défendre, et quand certains manques subsistent, la mauvaise fois de l'ancien utilisateur peut ressurgir.

Compilation, runtimes, et cibles additionnelles

OCaml dispose depuis sa genèse de deux cibles de compilation :

La présence d'une machine virtuelle a permis le développement du vénérable Js_of_OCaml qui permet la transformation dy bytecode OCaml vers JavaScript, rendant OCaml parfaitement viable pour le développement d'application dans le navigateur, mais aussi dans le runtime node, et qui est drastiquement utilisé pour ce site web. En utilisant une technique similaire, le support de WebAssembly a été rendu possible, très récemment, par le biais du projet Wasm_of_OCaml. Supporter la compilation vers WASM pour un langage étant doté d'un glâneur de cellules était un sacré challenge, mais l'équipe derrière WASM ayant récemment spécificé l'interaction entre WASM et des garbage collector, OCaml dispose maintenant d'une compilation vers WebAssembly parfaitement décente (et beaucoup de projets web, ambitieux, comme Ocsigen, commencent à supporter WASM nativement).

De plus, le projet Melange (historiquement BuckleScript) propose de transpiler — mapper l'AST de OCaml vers l'AST de JavaScript — est une alternative pour produire du JavaScript. Si je devais comparer Js_of_OCaml et Melange, au delà des différentes méthodes sous-jacentes utilisées pour produire du JavaScript (compiler vers du bytecode et transformer ce bytecode en JavaScript, contre la transformation syntaxique de OCaml en JavaScript), je dirais que Js_of_OCaml se marie mieux avec l'écosystème OCaml, et est donc, probablement, à destination de développeurs OCaml désireux de rendre leurs projets accessibles depuis un navigateur — en effet, l'interaction avec l'écosystème JavaScript existant peut-être plus laborieuse. Melange se marie mieux avec l'écosystème JavaScript (npm and co) et est donc, probablement, à destination des développeurs JavaScript désireux d'apporter plus de sûreté dans leurs projets JS (ou dans une base de code existante).

De nos jours, il est courant de trouver des langages multi-backend comme Idris ou Nim. Cependant, à l'époque, j'étais très impressionné par le fait que OCaml puisse, depuis le moment où j'ai commencé à l'utiliser, compiler vers JavaScript (en plus). A cette épqoue, je ne connaissais que Haxe qui proposait plusieurs cibles de compilation, si différentes (d'ailleurs, Haxe est écrit en OCaml).

En effet, en 2024, produire du JavaScript est devenu standard, on trouve les premières traces de Js_of_OCaml en 2006, faisant de OCaml, un pionier dans le domaine !

Un petit détour par MirageOS

Dans le treillis formé par les différents contextes d'exécution et de compilation de OCaml, avoir des bibliothèques qui fonctionnent bien dans une majorité de contexte est un exercice compliqué. Heureusement, le projet MirageOS — un ensemble de bibliothèques conçues pour construire un systéme d'exploitation dedié à ne faire tourner qu'une seule application, au moyen de virtualisation (un unikernel) — a introduit une véritable hygiène de la production de bibliothèques multi-contextes.

Dans un futur proche, j'aimerais passer plus de temps à écrire sur Mirage, un projet fascinant que l'on essaie d'intégrer dans nos projets, par exemple dans YOCaml, notre générateur de sites statiques. D'ailleurs, en plus de fournir une approche saine de la distribution de bibliothèques intelligemment compartimentées, Mirage offre un socle solide de bibliothèques pour la construction de projets OCaml, dont je parlerai, en étant plus expansif, dans la rubrique dédiée aux bibliothèques.

La plateforme OCaml

La plateforme OCaml est un ensemble d'outils, maintenus dans un cycle de vie explicite (actif, en incubation, maintenu et déprécié), destinés adosser le compilateur à une chaine d'outillage cohérente pour la production de code OCaml. On y trouve beaucoup d'outils qui servent différents propos, cependant, dans cette section, je ne vais me focaliser que sur certains points de la plateforme, vous laissant le loisir de consulter sa page et sa feuille de route pour de plus amples informations. Dans cette section, nous allons nous intéresser, dans les grandes lignes, à 4 grands points spécifiques :

Quand on utilise OCaml depuis un certains temps, c'est probablement la partie la plus excitante de l'article, car, selon moi, c'est celle qui a bénéficié du plus de progrès. Et la feuille de route est, selon moi, prometteuse !

OPAM, le gestionnaire de paquets

Même si les gestionnaires de paquets dédiés à un langage spécifique sont devenu très populaires (pour ne pas dire obligatoire) dans la réduction des frictions de l'adoption d'un langage, à l'époque ou OCaml a été conçu, ces derniers étaient confidentiels. En effet, mis à part CTAN, pour distribuer des paquets TeX et CPAN, inspiré par CTAN, pour distribuer des paquets Perl et PEAR, pour PHP, il faudra attendre les Gems pour que les technologies de développement considèrent l'adoption d'un gestionnaire de paquets comme axiomatique pour le développement d'un langage.

OPAM, pour OCaml Package Manager est une proposition de 2012 (la page À propos du site officiel présente une petite frise chronologique). OPAM permet, en plus d'installer des paquets, installer des versions différentes de OCaml, et construire des environnements potentiellement isolés dans des bacs à sables, que l'on appelle des switches. Il est possible d'utiliser le dépôt publique de ressources, hébergé sur Github mais il est parfaitement possible de construire son propre index de paquets.

Ayant déjà publié plusieurs paquets sur OPAM, je dois avouer que la CI de validation d'ajout de paquets est incroyablement efficace et ergonomique (chaque erreur fournit un Dockerfile pour reproduire l'issue localement) et que l'équipe de personnes qui modèrent et administrent les ajouts/modifications de paquets sont extraordinaires de réactivité et de bienveillance.

Même si, à la lumière de la modernité, on pourrait reprocher plusieurs points à OPAM, par exemple :

Je dois avouer qu'en venant d'une ère où OPAM n'existait pas, j'ai appris à m'accomoder de certains de ces petits écueils et que, quotidiennement, je dois avouer avoir peu de raison de me plaindre de l'outil qui, pour mon usage quotidien, ne m'a jamais réellement fait défaut. Cependant, si vous avez fait face à des soucis d'usages, je vous invite à venir en discuter sur l'un des espaces destinés à la communication pour que l'équipe de développement puisse tenir compte de vos retours, et vous aiguiller.

Il existe aussi esy comme gestionnaire de paquets alternatif, qui s'inspire de Nix pour construire un store réutilisable, de la même manière qu'il est possible d'utiliser Nix avec OCaml, cependant, étant un peu conventionnel, je ne suis pas vraiment aux faits de ces pratiques et, étant satisfait de mon workflow avec OPAM, je n'ai, malheureusement, jamais pris le temps d'expérimenter sérieusement esy.

Dune, le build-system

Comme pour la gestion de paquets, historiquement, OCaml disposait de plusieurs build-systems : le vénérable ocamlbuild, oasis, ocp-build, Jenga et d'autres variation autours de Make. Cependant, depuis 2018, la communauté à fortement adopté Dune, un build-system initialement développé à Janestreet.

Sur beaucoup d'aspects, Dune peut être intimidant. En effet, sa documentation est très touffue — mais elle s'est très largement améliorée, en terme de structure au cours de ces derniers mois. Et, alors que beaucoup d'outils choisissent des langages de descriptions de règles comme Yaml, Toml ou encore JSon, Dune a fait le choix des S-expression. On regrettera aussi que Dune paramètre, par défaut, l'ensemble des avertissements fatal.

Avant de motiver certains choix (comme les S-expression), il est très important de souligner les points qui ont fait de Dune un standard :

Peut-être que je suis biaisé mais, selon moi, Dune est un des build-system les plus génériques et agréables que j'ai pu utiliser — même si, aux premiers abords, il peut sembler effrayant et certains choix peuvent sembler difficile à motiver.

Sur le choix des S-expression

Aux premiers abords, l'utilisation d'un lisp-like pour décrire des binaires, des bibliothèques et des projets peut sembler surprenant. Cependant, cette décision à plusieurs avantages :

Donc, de mon point de vue, le choix des S-expression est pertinent, il permet de décrire des programmes complexes, lisibles, sans être trop verbeux, il ne pénalise pas trop la compilation et il permet de décrire, de manière très concise, des règles de compilations très complexes. Et pour être très honnête, on s'y fait très vite !

Contribution à l'état de l'art: Selective Applicative Functor

En plus d'être un très agréable build-system, Dune a contribué à l'état de l'art de la recherche en mettant en lumière une nouvelle construction inspirée de la théorie des catégories. En effet, en 2018, Andrey Mokhov, Neil Mitchell et Simon Peyton Jones ont proposé, dans l'excellent papier "Build Systems à la Carte", une collection d'abstraction pour réimplémenter — modulairement — des build-systems divers et variés. Cependant, pour certaines raisons liées à l'analyse statique des dépendances, ces modèles n'étaient pas compatibles avec Dune. Après plusieurs investigations et expérimentations, une nouvelle construction, similaire à un Applicative, un Selective Applicative Functor, capturant les pré-requis de Dune a été proposée. Cette information peut sembler anecdotique, mais, de mon point de vue, elle renforce l'intérêt (et l'importance) d'être à l'intersection entre la recherche et l'industrie.

Alternatives

Bien qu'étant fortement adopté par la communauté, OCaml propose des systèmes alternatifs (utilisant parfois Dune sous le capot), par exemple, Obazl qui offre des règles OCaml pour Bazel, Onix qui permet de construire des projets avec Nix, Buck2 qui est un projet ambitieux et générique qui est en compétition avec Bazel et Drom qui offre une expérience similaire à Cargo, unifiant la gestion de paquets et la construction de projets.

En toute transparence, je n'ai jamais expérimenté ces alternatives, étant très satisfait de Dune et de la direction qu'il est en train de prendre, unifiant enfin, la gestion des constructions et des paquets !

LSP et Merlin pour les éditeurs

Dans les précédentes sections, nous avons pu voir à quel point OCaml à progressé dans des domaines nécéssaires à l'industrialisation. Par contre, en terme de support éditeur, OCaml dispose depuis plus de 10 ans d'un excellent support pour Vim et Emacs au moyen du projet Merlin qui fournit des services d'éditeurs permettant de la complétion, du diagnostique, des fonctionnalités de navigation dans le code, des outils liées à la déstructuration de valeurs, à la construction de valeur, de la gestion (et navigation) de trous typés, de la recherche par polarité, des informations précises (avec contrôle de verbosité) sur les types de valeurs, du jump-to-definition etc.

Selon moi, le support IDE, via Merlin, est excellent, en OCaml, depuis très longtemps. Couplé avec ocp-indent, qui permet de calculer la position du curseur après une action dans l'éditeur et OCamlformat, qui permet le formatage (configurable) à la volée de document OCaml, écrire du code dans Emacs ou Vim est un immense plaisir !

Avènement de VSCode, LSP comme standard

En 2015, Visual Studio Code est arrivé en amenant Language Server Protocol permettant d'abstraire la manière dont les éditeurs interagissent avec un langage par le biais d'un serveur, respectant protocole uniforme. OCaml dispose d'un très bon serveur LSP qui, lui même, repose sur des bibliothèques éprouvées de l'écosystème OCaml, notamment Merlin. Comme LSP est devenu relativement standard dans le monde des éditeurs (Vim, Emacs et, en fait, presque tous les éditeurs libres que je connaisse) permettent d'interagir avec un serveur LSP, il est question de déprécier le serveur de Merlin, pour ne passer plus que par LSP, faisant de Merlin une bibliothèque bas-niveau, fournissant l'outillage, sous forme de bibliothèque utilisée par LSP. C'est un des projets sur lequel travaille l'équipe Editeur, chez Tarides (dont je fais partie) : rendre ocaml-lsp compatible en fonctionnalités, avec le serveur historique de Merlin pour réduire la maintenance des clients alternatifs (Emacs et Vim), ne nous souciant plus que des requêtes et actions spécifiques à OCaml (et ne faisant donc, logiquement, pas partie du protocole).

Un peu comme pour Dune, l'état de l'outillage est, à mon sens, excellent, et la feuille de route est motivante ! Cependant, comme c'est mon travail, il est probable que je sois biaisé.

Odoc, le générateur de documentation

OCaml est distribué avec un générateur de documentation, le vénérable OCamldoc, cependant, ce dernier n'est plus recommandé par/pour la communauté. En effet, l'outil mis en avant est Odoc, un nouvel outil, qui vit en dehors du compilateur et qui offre plusieurs fonctionnalités très intéressantes :

Même si le look'n feel d'une documentation générée par Odoc, par défaut, est, de mon point de vue, largement superieur de celle générée par OCamldoc, il reste tout de même (une fois de plus, de mon point de vue) un peu de travail à fournir sur l'UI pour que l'outil soit réellement parfait !

J'ai clairement une certaine sympathie pour la documentation du langage Elixir, HexDoc (en terme de design et de fonctionnalités), et à titre personnel, j'aimerais que OCaml converge vers cet exemple. En revanche, il faut reconnaitre que la documentation générée par Odoc est supérieure à beaucoup de documentation d'autres langages.

Bibliothèques disponibles

On a vu que le langage était cool, et qu'il dispose d'un outillage, bien que toujours en progrès, efficace et agréable à utiliser. Se pourrait-il que son manque de popularité soit la conséquence d'un ensemble de bibliothèque trop restreind ? Pour être très honnête, je ne sais pas. Ce que je sais, c'est que quand j'ai eu à écrire des projets OCaml, professionnels comme personnels, j'ai souvent trouvé tout mon bonheur dans la liste des paquets. Je pense que les raisons qui font que OCaml est mature pour beaucoup de projets usuels peut se synthétiser en plusieurs points :

Pour ma part, il m'est arrivé de re-créer des bibliothèques pour le plaisir de réinventer la roue, mais aussi, parfois, pour proposer une interface alternative. De plus, OCaml permet de s'interfacer avec, entre autres, du C, permettant de construire des bindings pour un grand nombre de bibliothéques et outils. Cependant, s'il existe une bibliothèque que vous trouvez objectivement manquante, je vous invite à prendre part à la communauté.

Il est important de noter que mon usage de OCaml s'est porté essentiellement sur 3 sujets :

Tous ces sujets impliquent tout de même la nécéssité d'un bon outillage de tests et OCaml dispose de plusieurs bibliothèques complémentaires pour implémenter des suites de tests robustes. En effet, dans l'écosystème OCaml on trouve de quoi rédiger des doctests, des tests unitaires classiques, de quoi décrire des tests dirigés par les propriétés, de quoi faire du fuzzing mais aussi des tests par observation de la sortie, des tests inlines (qui permettent de tester, notamment, des composants privés) et des tests cram.

Je continue de trouver mon bonheur dans les paquets disponibles et je suis toujours très impressionné de voir le nombre de paquets et d'alternative croître, d'année. en année. Il existe évidemment des manques, mais qui n'ont pas invalidés mon choix de OCaml.

Aparté sur la bibliothèque standard

Un reproche récurrent qu'il est fait à OCaml est la modestie de sa bibliothèque standard. En effet, historiquement, cette dernière ne servait qu'a implémenter le langage. Elle ne s'ecombrait donc pas de certaines fonctionnalités utiles pour l'utilisateur final. Cette situation a engendré l'émergence de bibliothèques standards alternatives dont les plus populaires sont :

Je n'ai pas d'opinion forte concernant le choix d'une bibliothèque alternative, j'ai tendance à utiliser celle que mon projet utilise ou de réinventer la roue (parce que c'est très rigolo), cependant, si je devais donner un avis, il est probable que je recommande Containers.

En plus de ces bibliothèques standards alternatives, on trouve des bibliothèques spécifiques qui résolvent des problématiques générales comme Bos qui propose des outils pour interagir avec un système d'exploitation ou encore Prefaceshameless plug — qui permet de concrétiser des abstractions issues de la théorie des catégories.

La position des mainteneurs sur la bibliothèque standard à évolué au fil des années et il est dorénavant envisageable de l'étendre. Cependant, les additions dans cette dernière sont souvent sujettes à débat et l'addition de nouveaux modules peut parfois prendre beaucoup de temps. Pour ma part, j'aurais préféré que la bibliothèque standard continue à ne servir que le développement du langage et qu'une bibliothèque sous l'ombrelle de la communauté OCaml soit publiée. Cette séparation permet de désynchroniser les releases du langage et de sa bibliothèque standard et aussi, probablement, de simplifier la compatibilité entre cette bibliothèque et le langage.

Conclusion de l'écosystème

Je n'ai malheureusement pas l'occasion de parler de tous les outils de la plateforme, ni des briques fondamentales qui font que OCaml est un langage agréable à utiliser pour des projets personnels, mais aussi pour des projets industriels (par exemple, des différents débogueurs existants). En revanche, j'espère avoir survolé quelques outils, qui forment un socle solide pour l'utilisation de OCaml.

Dans mon utilisation du langage, il m'est parfois arrivé de devoir construire ma propre bibliothèque, cependant, ce n'est pas un exercice que je regrette et je pense que, maheureusement, si l'on décide de ne jamais utiliser un langage parce que 100% des bibliothèques nécéssaires ne sont pas disponibles, je trouve, peut-être maladroitement que c'est nivellement par le bas, et que ça nous enferme derrière des langages portés par des entreprises riches, comme Java ou C# et c'est un peu triste.

Sur la communauté

Même si j'ai utilisé beaucoup de langages de programmation différents, je pense que OCaml est le seul avec lequel j'ai entretenu une interaction communautaire forte. Je ne suis donc pas au courant de la manière dont les choses se passent dans d'autres communautés, ce qui rend mon retour un peu inutile. Mais de mon expérience, je trouve que la communauté OCaml, en plus d'être très productive, est :

Pour conclure sur le pan communautaire, même si je ne suis pas réellement aux faits des interactions dans d'autres communautés, je trouve que c'est un plaisir de faire partie de celle des développeurs et développeuses OCaml. C'est un espace bienveillant, propice au partage et à l'apprentissage.

Quelques mythes liés à OCaml

J'arrive — enfin — à la partie la plus amusante de ce trop long article, je vais pouvoir debunker certains mythes persistants liés à OCaml. Je ne promet toujours de bonne foi, mais sachez que mon intention est bonne. On lit souvent sur les internets plusieurs critiques ou remarques à l'égard d'OCaml, et souvent, je trouve qu'il est fatiguant d'y répondre. Cependant, quoi de mieux qu'un article destiné à faire part de mon intérêt pour le langage pour prendre le temps de survoler certaines de ses critiques et tâcher d'apporter une réponse ?

J'en ai sélectionné quelques-uns mais il est probable que dans le futur, j'écrirais des articles un peu plus long, à la manière des membres de HeyPlzLookAtMe, sur des articles que je trouve injustes.

OCaml et F#

F# est un langage de programmation historiquement très inspiré par OCaml qui tourne sur la plateforme .NET (et s'interface, de facto, très bien avec C#). Je trouve le langage — que j'ai professionnellement utilisé chez DernierCri et chez D-Edge — très agréable. Comme historiquement, .NET était exclusivement réservé à des environnements Windows, OCaml souffrait peu de la comparaison, cependant depuis l'arrivée de .NET Core, une implémentation multi-plateforme de .NET, il m'arrive de plus en plus de lire sur les internets, de phrases de ce genre :

Pourquoi continuer à faire du OCaml, quand on peut disposer du même langage, F#, avec tout l'écosystème .NET, plus de fonctionnalités et une syntaxe plus agréable à utiliser.

Premièrement, je trouve que oui, bénéficier de l'écosystème .NET (Core) est un énorme avantage. Concernant le syntaxe, je suis plus réservé. En effet, je trouve que la syntaxe basé sur l'indentation rend parfois le déplacement de code plus laborieux et même s'il existe des critiques à l'encontre de la syntaxe de OCaml, je dois avouer qu'elle ne me fait pas défaut. Le dernier point me semble, lui, un peu plus pernicieux. En effet, F# s'est vu doté de fonctionnalités non présentes dans OCaml, par exemple :

Ces évolutions sont arrivées progressivement dans le langage. Il serait naïf de croire que OCaml n'a pas évolué lui aussi. En effet, bien que historiquement, les deux langages semblaient très similaires, dès le début de la proposition de F#, certaines fonctionnalités étaient manquantes :

Ces deux raisons, à elles seules, suffiraient de considérer OCaml et F# comme deux langages cousins mais très différents et motiveraient, selon moi, très largement le fait de préférer l'un à l'autre. Dans mon cas, OCaml plutôt que F# rendant la phrase d'introduction de la section caduc. Cependant, comme F#, OCaml a aussi évolué et en plus de ces deux différences fondamentales, on trouve beaucoup de choses en OCaml, absente dans F# :

Pour conclure, même si F# est un vraiment chouette langage, et que son usage fait profiter de beaucoup d'avantages (notamment la plateforme .NET), ce n'est pas juste une meilleure version de OCaml. Les deux langages sont très différents et, de mon point de vue, OCaml dispose d'un système de types plus sophistiqué, me faisant le préférer largement à F#. Et, à mon sens, dire que F# est juste un OCaml plus beau est aussi recevable que de dire que Kotlin n'est rien de plus qu'un Scala avec une syntaxe plus légère.

Les opérateurs doublés pour les flottants

Dans la bibliothèque standard, on trouve les opérateurs arithmétiques sur les entiers suivant :

val ( + ) : int -> int -> int
val ( - ) : int -> int -> int
val ( * ) : int -> int -> int

Mais aussi des opérateurs arithmétiques pour les nombres flottants :

val ( +. ) : float -> float -> float
val ( -. ) : float -> float -> float
val ( *. ) : float -> float -> float

Aux premiers abords, cela peut sembler déroutant. Cependant, c'est parfaitement sensé. Si on voulait avoir des opérateurs génériques, il nous faudrait du polymorphisme ad-hoc, comme c'est le cas en Haskell, par exemple, où les opérateurs arithmétiques résides dans la classe Num :

class  Num a  where
  -- more code
  (+), (-), (*) :: a -> a -> a
  -- more code

Sans forme de polymorphisme ad-hoc (via des classes, des traits ou des implicites), permettant de décrire une contrainte sur nos opérateurs : op :: Num a => a -> a -> a ? Une proposition que j'ai souvent lu sur internet serait d'utiliser la même triche que pour l'opérateur =, dont le type est val (=) : 'a -> 'a -> 'a. Ça ne fonctionne pas, parce que alors que l'on peut espérer que tout soit comparable (dans le pire des cas, on peut renvoyer false), comment généraliser, par exemple, une addition ?

Le support des opérateurs arithmétiques est un problème laborieux, qui est d'ailleurs la motivation originale derrière les classes de types (et la raison d'être des Paramètres de type résolus statiquement en F#). De mon point de vue, en attendant les modules implicites, doubler les opérateurs pour fonctionner avec les entiers et les flottants me semble être une proposition raisonnable et, si pour d'étranges raisons, suffixer les opérateurs par des points en présence de flottant vous donne de l'urticaire, il est possible, au moyen d'ouvertures locales, de s'en passer en fournissant, par exemple, ce module :

module Arithmetic (P : sig
  type t

  val add : t -> t -> t
  val sub : t -> t -> t
  val mul : t -> t -> t
  val div : t -> t -> t
end) =
struct
  let ( + ), ( - ), ( * ), ( / ) = P.(add, sub, mul, div)
end

Qui permet d'étendre les modules Int et Float (qui disposent déjà des fonctions add, sub, mul et div) pour les provisionner d'opérateurs arithmétiques :

module Int = struct
  include Int
  include Arithmetic (Int)
end

Dans les grandes lignes, on construit un module Int, on inclut le module Int précédent, pour que notre nouveau module Int dispose de toute l'API du module Int original, ensuite on construit (et on inclut) nos opértateurs arithmétiques. On peut maintenant répéter l'opération avec Float :

module Float = struct
  include Float
  include Arithmetic (Float)
end

Et on peut maintenant se servir de l'ouverture locale pour ne pas devoir suffixer nos opérateurs avec des points :

let x = Int.(1 + 2 + 3 + (4 * 6 / 7))
let y = Float.(1.3 + 2.5 + 3.1 + (4.6 * 6.8 / 7.9))

De mon point de vue, même si ça peut être déroutant quand on vient de languages pour lesquels ce n'est pas une question, c'est un problème mineur, et il me semble que l'absence de surcharge d'opérateurs est un argument un peu léger pour ne pas donner sa chance à un langage, mais ce n'est que mon humble avis.

Sur la séparation entre ml et mli

Un autre point qui fait couler beaucoup d'encre (encore récemment) porte sur la séparation entre ml et mli. Pour ma part, je trouve ça génial, même si ça peut engendrer une légère forme de répétition, je peux me concentrer sur l'API, via l'encapsulation, de mon module côté mli tout en décrivant de la documentation, ça me permet d'ordonner les fonctions que j'expose dans l'ordre qu'il me plait et, naturellement, ça me fait abstraire au maximum les types que je partage. De plus, si je consulte une implémentation, le code du ml est rarement pollué par de la documentation et je peux rapidement naviguer dans les différentes éléments qui consistuent le module que j'observe. En plus ça rend la compilation séparée possible et permet de ne pas recompiler des modules qui dépendent de modules dont seul l'implémentation a été modifiée en développement (c'est le comportement par défaut de Dune dans le profile dev).

Cependant, les goûts et les couleurs ne sont pas vraiment discutable et quand on expose des types complexes ou des types de modules, cette répétition peut être ennuyante. Heureusement, il existe une astuce, présentée en 2020 par Craig Ferguson, qui permet de palier à ces répétitions : The _intf_ trick.

En plus, il existe des petites astuces, basées sur la possibilité de passer des expressions de modules arbitraires aux primitives open et include qui permettent, parfois, de se passer mli. J'en avais déjà parlé dans l'article OCaml, modules et schémas d'importation.

Gérer l'encapsulation sans mli

On peut simplement utiliser open struct (* code privé *) end pour ne pas exporter des parties codes sans nécéssiter d'interfaces. Par exemple :

open struct
  (* Private API *)
  let f x = x
  let g = _some_private_stuff
end

(* Public API *)
let a = f 10
let b = g + 11

Exprimer l'interface depuis le ml

Une autre technique similaire consiste à utiliser include (struct ... end : sig (* API publique *) end) pour permettre de décrire la structure et l'interface dans le même fichier. Par exemple :

include (struct
  type t = int
  let f x = x
  let g = _some_private_stuff
end : sig
  type t
  val f : int -> t
end)

De cette manière, la signature et la structure vivent dans le même fichier, tout en permettant de contrôler précisemment l'encapsulation. Une autre approche consisterait à sortir la signature dans un module type dédié, de cette manière :

module type S = sig
  type t
  val f : int -> t
end

include (struct
  type t = int
  let f x = x
  let g = _some_private_stuff
end : S)

C'est très similaire à la première proposition, si ce n'est que le module expose aussi le type de module S. L'effet de bord bénéfique de ce leak est que l'on peut facilement référencer la signature du module en utilisant My_mod.S plutôt que de devoir utiliser module type of My_mod.

Pour conclure sur la séparation

Je trouve que cette séparation est très désirable, cependant, le langage de module de OCaml étant très expressif, il est possible, au moyen d'un peu d'encodage, de pallier à cette séparation. De mon point de vue, ces approches servent essentiellement à démontrer cette expressivité parce que le contre-coup de ce regroupement est la perte de la compilation séparée, ce que je trouve sacrément balot.

Pour conclure

Je pense avoir sommairement survolé les points que je voulais développer. De mon point de vue, OCaml est un langage génial ! En effet, il offre un très bon compromis entre la sûreté et l'expressivité notamment grâce à un système de type très avancé, un langage de modules riche, des objets, le support de row polymorphism au moyen d'objets et de variants polymorphes et le support d'effets définis par l'utilisateurs ! Son intersection entre la recherche et l'industrie en font un langage qui, de mon point de vue, évolue dans la bonne direction, en intégrant précautionneusement de nouvelles fonctionnalités permettant au langage de rester moderne, sans subir les éceuils d'une intégration trop rapide et non éprouvée.

Même si pendant plusieurs années, l'outillage de OCaml pouvait sembler un peu ... poussiéreux, ces derniers temps, notamment grâce au support commercial de certaines entreprises, l'outillage s'est drastiquement modernisé, et continue son progrès comme le témoigne la feuille de route de la plateforme. En complément, l'ensemble des bibliothèques disponibles progresse permettant d'utiliser OCaml dans une multitude de contexte, notamment grâce à ses différentes cibles de compilation (par exemple le navigateur via js_of_ocaml et wasm_of_ocaml).

En ajoutant à un langage expressif, doté d'un écosystème versatile, une communauté bienveillante et réactive, OCaml devient un choix très sérieux pour des projets personnels et professionnels. Il est évident que proposer une migration complète d'une base de code pour aller vers OCaml n'est probablement pas un choix pragmatique, mais si vous avez des petits projets personnels en tête et que vous êtes curieux et amusés par les langages de programmation, je vous invite sérieusement à considérer OCaml !

J'espère avoir réussi à transmettre mon intérêt pour ce langage (et son écosystème). Si vous désirez en parler, trouver des projets ou des points de contributions, je serais ravi d'en parler avec vous, ou alors vous pouvez vous adresser à la communauté par le biais du forum qui est actif, réactif et bienveillant !


Commentaires

Comme mon site web est hébergé sur un serveur de fichiers statiques (et ne dispose pas de base de données), les commentaires sont assurés par le Fediverse. Si vous possédez un compte, par exemple Mastodon, vous pouvez donc commenter cette page en répondant à ce fil de discussion!