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.
Sur le choix d'OCaml
- Avant-propos
- OCaml en tant que langage
- OCaml en tant qu'écosystème
- Sur la communauté
- Quelques mythes liés à OCaml
- Pour conclure
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 :
-
"Why OCaml?", prologue du livre Real World OCaml, qui présente des avantages factuels à l'utilisation d'OCaml (et dont l'introduction propose une frise chronologique). Même si le livre est excellent sur de nombreux aspects, j'ai pris l'habitude de ne pas le recommander car je le trouve très biaisé dans son usage, proposant l'utilisation de bibliothèques, par défaut, qui ne font pas spécialement l'unanimité dans la communauté.
-
"Better Programming Through OCaml", prologue du livre (accompagné de vidéos) OCaml Programming: Correct + Efficient + Beautiful qui présente essentiellement en quoi l'apprentissage d'OCaml peut améliorer les compétences d'un développeur dans d'autres technologies plus populaires. Le livre est assez récent et c'est celui que j'ai pris l'habitude de recommander comme ressource de base pour appréhender OCaml.
-
Conférence "Why OCaml?", une conférence de Yaron Minsky, le CTO de l'entreprise Jane Street — un utilisateur industriel d'OCaml faisant partie des leaders mondiaux de la finance. Yaron est aussi l'un des auteurs de Real World OCaml et la personne à qui l'on doit la très populaire phrase, dans le monde des langages de programmation à vérification statique des types, "Make illegal states unrepresentable". La conférence donne beaucoup d'informations sur les motivations du choix d'OCaml chez Jane Street.
-
"OCaml for Fun & Profit: An Experience Report", présentée par Tim McGilchrist durant la conférence Yow 2023, qui, après une présentation riche du langage, expose certains cas d'usages très concrets de l'utilisation d'OCaml en production, avec fun et profit.
-
"Replacing Python for 0Install" par Thomas Leonard. Cette série d'articles est, de mon point de vue, incroyablement intéressante. En effet, l'auteur de 0Install, un système d'installation de logiciels décentralisé et multiplateforme (une alternative très légèrement plus ancienne que Nix), cherche un autre langage que Python pour l'implémentation d'une nouvelle version (le remplacement de Python est, lui aussi, documenté) et procède très consciencieusement à la comparaison méthodique de plusieurs candidats : ATS, C#, Haskell, Go, Rust et OCaml avec Python. Plusieurs années après, je suis toujours émerveillé par la rigueur et la nuance de cette série que je recommande très fortement.
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 :
-
Une guidance sur les fonctionnalités désirables comme objet de langage intéressant, supportées par une recherche poussée. Par exemple, à ma connaissance, OCaml est le premier langage mainstream proposant un support natif des effets définis par l'utilisateur (user-defined effects), qui est le fruit d'une recherche avancée, illustrée par beaucoup de publications.
-
Une guidance sur les fonctionnalités désirables comme outil pour l'industrialisation, aussi supportées par une recherche poussée et motivées par des cas d'usages. Par exemple, depuis peu, Jane Street, un des utilisateurs industriels très importants de OCaml, a proposé l'intégration de sessions affines permettant un contrôle linéaire des ressources (un peu à la Rust).
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 :
-
La lisibilité d'une implémentation. En effet, il arrive parfois que pour éviter la mutabilité, il faille ajouter de la plomberie additionnelle (par exemple, une monade State) rendant la lecture et la compréhension d'un programme plus laborieuse.
-
La performance. L'ajout de plomberie peut engendrer des coûts, rendant l'exécution d'implémentations plus laborieuse.
-
Le confort à l'usage. Il y a quelques années, Arthur Guillon m'avait cérémonieusement dit que "OCaml était un lambda-calcul permettant trivialement d'exécuter des effets", ce qui le rendait très efficace pour, par exemple, faciliter le débogage en permettant facilement d'imprimer des messages sur la sortie standard. Même si je reconnais que ce n'est probablement pas la meilleure manière de produire de la journalisation, cela apporte indéniablement un confort d'utilisation, permettant le prototypage rapide.
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é :
-
Le socle des fonctionnalités n'a pas été pensé pour l'industrie. Cependant, cette assertion n'est plus du tout vraie, essentiellement parce que OCaml est devenu un langage utilisé industriellement. Même si, dans la genèse du langage, on trouvait plus d'outils pour construire un langage (permettant de faciliter l'enseignement du fonctionnement des compilateurs plus aisé) que de l'outillage pour construire des applications dites "entreprises", des projets issus de la communauté, motivés par des usages industriels enrichiront le langage et son écosystème, faisant du langage un outil générique, et adapté à l'industrie. Par exemple, la construction d'une liaison avec la bibliothèque Tk motivera l'intégration, dans le langage, d'arguments nommés, d'arguments optionnels, et de variants polymorphes.
-
L'ensemble des paradigmes et des fonctionnalités du langage sont très mûrement réfléchis et théorisés. En général, l'intégration d'une fonctionnalité (ou d'une collection de fonctionnalités) est le fruit d'un travail de recherche méticuleux, basé sur des fondements théoriques solides et soumis à la revue d'un grand nombre d'experts dans le domaine (souvent reconnus par la communauté scientifique). Cette rigueur peut parfois ralentir l'intégration de nouvelles fonctionnalités, mais garantit généralement leur bon fonctionnement et leur stabilité théorique.
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 :
-
Des types produits : qui permettent de grouper des valeurs de types hétérogènes (donc de créer une conjonction de types hétérogènes). Ils sont généralement présents dans tous les langages mainstream (les objets par exemple, qui introduisent des concepts en plus, ou les couples et les enregistrements).
-
Des types sommes : qui permettent de construire une disjonction de types de valeurs hétérogènes, des différents cas, indexés par des constructeurs. Même si on peut trouver des cas particuliers de sommes dans les langages mainstreams, notamment les booléens (qui sont une disjonction de deux cas :
true
etfalse
, soit deux constructeurs sans paramètres), le support de ces dernières est souvent laborieux dans les langages populaires. Par exemple, Kotlin et Java (et de facto, C#) utilisent une construction, associée aux relations d'héritages, le scellage. L'intégration d'une syntaxe dédiée aux sommes a aussi pris un peu de temps en Scala, qui, avant les dernières versions du langage, utilisait aussi des familles scellées, rendant l'expression de sommes assez verbeuses (et, de mon point de vue, difficile à raisonner). -
Des types exponentiels : qui permettent de décrire des fonctions qui permettent d'exprimer des types pour des fonctions d'ordre supérieur (que l'on peut passer en argument ou renvoyer).
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 compilation séparée, une fonctionnalité clé, permet de compiler efficacement de gros programmes en identifiant des points de jonction pour optimiser la compilation parallèle et incrémentale. Cette approche est exploitée par dune, le système de construction recommandé pour compiler du OCaml.
-
La séparation systématique entre l'implémentation et l'interface offre plusieurs avantages significatifs, notamment l'encapsulation et la localisation de la documentation dans l'interface. Dans mon flot de programmation, je trouve ça très confortable car je peux implémenter ma structure (l'implémentation d'un module) en me laissant guider par l'inférence et spécifier son API dans sa signature (l'interface du module) tout en décidant d'un ordre d'affichage et en fournissant une documentation claire ne polluant pas l'espace d'implémentation. En complément, l'encapsulation me permet librement de décrire, dans le corps de ma structure, des types intermédiaires pour, par exemple, exprimer la machine à état d'une application, sans la laisser s'échapper.
-
Un outil formidable pour décrire des structures de données. En effet, en abstrayant les types (en cachant leur implémentation), couplé à l'encapsulation, il est possible de décrire des structures de données qui maintiennent des invariants. C'est d'ailleurs pour ça qu'il est courant d'avoir une paire structure/signature par structure de données cachant, au moyen de l'abstraction et de l'encapsulation, des détails d'implémentation.
-
De la réutilisabilité et de la mutualisation. En effet, de la même manière qu'il est possible de décrire des types dans le langage des valeurs (comme nous l'avons vu dans la rubrique dédiée aux types algébriques), il est possible de décrire des types dans le langage des modules, que l'on appelle des signatures translucides, permettant de décrire le type d'une signature, sans l'associer à une structure. Ces signatures sont typées structurellement, et couplées avec les fonctions dans le langage des modules, foncteurs, il est possible de mutualiser du comportement attaché à des modules.
-
Des formes de polymorphisme avancé, notamment du Higher Kinded Polymorphism, disponible dans le langage des modules. Dans les grandes lignes, on peut décrire "des génériques, paramétrés par des génériques". Cette limitation dans des langages, comme F# ou Java, motive souvent l'utilisation d'encodages lourds pour pallier ce manque.
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 :
-
un travail de fond sur l'expression des effets, avec une nouvelle syntaxe fraichement ajoutée, et une collection de travaux sur la séparation entre les opérations et les effets et, évidemment, sur la propagation des effets dans le système de types.
-
Jane Street a proposé un modèle non-intrusif de gestion de ressources, inspiré par celui de Rust, introduisant des modalités et un peu de linéarité.
-
Un véritable travail de fond à été entamé sur le langage de modules permettant de rendre l'implémentation de Modular Implicits plus sereinement implémentable.
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 :
-
l'absence de polymorphisme ad-hoc. Même s'il est possible de s'en tirer sans, notamment au moyen d'ouvertures locales de modules, l'absence de polymorphisme ad-hoc (au moyen de classes de types, à la Haskell ou traits/objets implicites, à la Rust et Scala ou encore des structures canoniques à la Coq) peut parfois rendre certaines situations délicates. Même si j'ai tendance à toujours trouver les relations explicites préférables, j'ai, au fil des années, trouvé plusieurs cas où cette absence pouvait être problématique :
-
l'impossibilité de décrire des contraintes de paramètres de types sur des fonctions polymorphes, introduisant dans la bibliothèque des fonctions d'égalités et de comparaisons polymorphes, faisant couler beaucoup d'encre et imposant, par exemple, des versions spécialisées de opérateurs arithmétiques pour les differentes représentations des nombres (
int
,int64
,float
). -
Le risque d'explosion combinatoire quand on décrit beaucoup de relations entre des modules. C'est pour cette raison que la bibliothèque Preface propose une découpe modulaire un peu complexe
Cependant, même si l'arrivée des modules implicites n'est probablement pas dans la feuille de route court-termiste, les récents travaux sur le langage de modules, présentés dans la rubrique dédiée au futur du langages, sont prometteurs.
-
-
Une interaction laborieuse entre le langage de modules et le langages des valeurs. En effet, le langage de modules est un langage différent, doté d'un système de types différent. Je ne sais pas si l'on peut réellement parler d'un point faible, mais cette différence peut être intimidante et s'explique par le fait que le langage de modules de OCaml est un pionier dans la théorie des modules et prédate des innovation plus récentes (1ML par exemple). Dans la pratique, en plus d'être complexe à appréhender, certains pans du langages sont difficiles à spécifier correctement, par exemple les modules récursifs.
-
Un langage confortable pour la programmation fonctionnelle, impur. Même si je trouve que l'impureté est une feature, quand on essaye d'importer des idiomes issus de langages purs (au hasard, Haskell), on peut se heurter à des difficultés liées à l'inférence de types : la value restriction. Même si, en OCaml, elle a été détendue, ses implications sur l'inférence de fonctions polymorphes peuvent être intimidantes — pour de très bonnes raisons.
-
La syntaxe. Même si, à titre personnel, j'apprécie beaucoup la syntaxe de OCaml et que je suis convaincu que la syntaxe devrait rarement être une issue, je suis conscient que certains choix syntaxiques peuvent être déroutants. Par exemple, le fait que les paramètres de types préfixent le nom du type, une liste de a sera écrite
'a list
. Beaucoup de ces choix sont motivés par une volonté de réduire l'ambiguïté de la syntaxe du langage et on s'y fait très vite. Cependant, je suis conscient qu'en venant d'un autre langage, certains de ces choix peuvent sembler surprenants.
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 :
-
dans le contexte professionnel, il est évident que si je veux que mon équipe et moi soyons productifs, il n'est probablement pas très pertinent de devoir construire une pile d'outils avant de pouvoir commencer à répondre au problème pour lequel on est mandaté.
-
Dans le contexte personnel, même si l'on pourrait arguer que construire sa pile technologique est très formateur, ça modifie l'ensemble des compétences que l'on veut travailler. Si pour construire une petite application web pour m'initier à OCaml comme un langage web, je dois construire toute ma pile HTTP, il est fort probable que OCaml ne soit pas le bon choix. Rassurez-vous, cependant, OCaml dispose d'un outillage riche pour construire des applications web !
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 :
-
la pertinence du runtime (ou des cibles de compilations) pour le projet. Il est probable que je ne recommande pas OCaml pour l'embarquer dans un tout petit hardware exotique — même si, n'y connaissant rien en programmation bas-niveau (parce que ce n'est pas du tout mon métier), il est probable que je me trompe.
-
Sa plateforme. Est-ce que l'ensemble de sa chaine d'outillage est complète et ergonomique. Ce qui inclut, de mon point de vue, un gestionnaire de paquet, un build-system, un bon support éditeur (agnostique au possible), un bon générateur de documentation et une collection d'outils additionnels, comme, par exemple, un formateur (et bien d'autres).
-
La pertinence des bibliothèques disponibles (et leur niveau de maintenance et leur découvrabilité, ce qui implique généralement la nécéssité de disposer d'un gestionnaire de paquets) avec une considération particulière sur l'ergonomie de ces dernières. Par exemple, si je ne dispose d'aucune primitive de chiffrement, il est probable que je ne choisisse pas cette technologie pour construire une blockchain. Il existe toute une classe de problèmes qu'il est très difficile de résoudre dans son coin ou dans un contexte professionnel.
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 :
-
une compilation native, qui produit des exécutables très efficaces, compilés pour une architecture. (Et qui supporte un grand nombre d'architectures). De plus, alors qu'historiquement, Windows était fortement délaissé, un effort tout particulier a été mis en oeuvre pour le supporter (on notera aussi le projet DkMl, une initiative indépendante).
-
une compilation vers un bytecode (donc à destination d'une machine virtuelle), produisant des exécutables portables.
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 :
- Le gestionnaire de paquets
- Le système de construction (build-system)
- Le support éditeurs (incluant le formatage de code)
- Le générateur de documentation
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 :
- une terminologie pouvant être laborieuse à appréhender (switch, invariant, etc.)
- la duplication de tous les paquets et de tous les compilateurs entre plusieurs switches (c'est un problème connu pour lequel du travail a déjà été mis en oeuvre)
- et probablement quelques soucis d'ergonomie (notamment l'interaction
avec
dune
pouvant être plus smooth, pour lequel un travail est aussi actuellement en cours) - quelques complications quand il s'agit de gérer des paquets en développement, en les référençant depuis un dépôt de source plutôt que depuis OPAM
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 :
- Dune est très rapide et propose un modèle d'exécution très efficace
- il construit des artéfacts nécéssaire à la configuration gratuitement
- il génère certains fichiers redondants (comme les fichiers de description OPAM)
- il trivialise le vendoring de bibliothèques
- il permet d'invoquer des boucles d'interaction correctement provisionnées par le contexte
- on se familiarise très rapidement avec les S-expression, qui permette de décrire schématiquement et rapidement des règles de compilation
- il est relativement agnostique et peut exécuter des tâches
arbitraires (à la manière de
make
) - il est en constante évolution et amélioré de version en version
- adossé à dune-release, il rend la publication de paquets sur OPAM incroyablement simple
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 :
- l'AST des S-expression étant drastiquement simple, le parsing est très simple et il est facile de le rendre très efficace, ce qui ne pénalise pas l'efficacité de la compilation
- le langage dispose de terminaison, ce qui le rend plus facile à inspecter en cas d'erreurs (pour toute personne ayant tenté de traiter des erreurs dans de gros fichiers YAML doit avoir été confronté à ce genre de problèmes)
- le langage est très facile à apprendre, et à décrire
- il permet de décrire de véritables programmes, rendant Dune relativement générique et permettant de faire des tâches additionnelles
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 :
- un langage de markup riche, supportant les références croisées
- la possibilité d'écrire des pages "de manuel", volatiles, tout en bénéficiant des références croisées
- une très bonne intégration dans Dune
- une barre de recherche par types (implémentée via Sherlodoc)
- l'inclusion du code source (rédigé dans la documentation ou des modules documentés)
- l'implémentation de dérivers permettant de générer des grands ensembles de documentation (utilisé pour implémenter la documentation de tous les paquets présents sur OPAM)
- le support de doctest via mdx
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 :
- des entreprises comme Lexifi et Janestreet ont fortement contribué à l'écosystème en libérant beaucoup des bibliothèques nécéssaires à leurs usages quotidien
- Des projets de recherches ambitieux, comme, dans le cas du Web par exemple, Ocsigen, utilisé industriellement dans le projet BeSport ont généré une collection de bibliothèques utiles
- Nous en parlions précédemment mais MirageOS, dans son approche Clean Slate a, naturellement, engendré beaucoup de bibliothèques robustes
- Comme pour des langages populaires, comme JavaScript ou Rust, des contributeurs motivés ont fourni d'excellentes bibliothèques
- Le langage est ancien, et utilisé industriellement depuis bien longtemps
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 :
- le développement web (fortement porté par Mirage, Ocsigen et des projets indépendants, comme Dream, YOCaml et bien d'autres)
- le développement de Blockchain et par extension l'utilisation de bibliothèque de cryptographie, offertes, une fois de plus, par Mirage, mais aussi par le projet HACL*, une bibliothèque formellement vérifiée, écrite en F* et extraite en OCaml
- le développement de Merlin
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 :
- Batteries, une alternative à la bibliothèque standard un peu datée. Historiquement une fork de Extlib.
- Base, une alternative
construite par Janestreet utilisée un peu
invasivement dans le livre Real World
OCaml. La bibliothèque utilise des
conventions forte, comme labeliser les fonctions d'ordre superieur
(généralement avec le nom
f
). - Core est une extension de Base.
- Containers est une
extension de la bibliothèque standard (dans le sens où
open Containers
en début de module ne casse pas du code rédigé avec la bibliothèque standard).
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 Preface — shameless 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 :
-
Très accessible : comme beaucoup d'autres, OCaml dispose d'une présence numérique forte. Sur ces différents espaces, il est possible d'y retrouver des contributeurs au langage et à son écosystème très aguéris et de bénéficier de conseils pointus (ou moins). Je me permet d'adresser une mention particulière à Gabriel Scherer et Florian Angeletti dont les réponses sont toujours élaborées et intéressantes.
-
Très bienveillante : il m'arrive souvent d'avoir à demander de l'aide et j'ai toujours eu l'occasion d'avoir des réponses claires et précises, que ça soit en privé ou en public.
-
Très brillante : OCaml est le fruit du travail de chercheurs brillants et avoir l'opprtunité d'interagir avec eux est incroyable (et potentiellement un peu intimidant). Avoir l'opportunité de poser, directement, des questions à des gens étant derrière certaines découvertes importantes de la construction de langage est une aubaine formidable.
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 :
- les expressions de calcul (qui sont une forme syntaxiquement plus générale que les opérateurs de liaisons)
- le fournisseur de types (qui malheureusement peut soulever quelques déconvenues avec .NET Core, dans certains cas de résolutions de noms/chemins)
- les motifs actifs
- Paramètres de type résolus statiquement
- la possibilité d'attribuer des méthodes à des sommes et des produits, ce qui, pour des raisons d'interopérabilité a beaucoup de sens, mais casse notablement l'inférence
- et probablement d'autres fonctionnalités que je ne connais pas bien (ou liées à l'interopérabilité avec la plateforme .NET, notamment de la réflexion)
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 :
-
l'absence d'un langage de modules. En effet, le mot-clé
module
est présent en F#, cependant, ce dernier ne sert qu'a décrire des classes statiques (et se marie étrangement avec les espaces noms) -
Un modèle objet drastiquement différent (pour l'interopérabilité avec C#, évidemment)
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# :
- les ouvertures locales et généralisées. En effet, en F#, on ne peut ouvrir un module qu'en top-level, ce qui parfois peut être très iritant
- Du row polymorphism sur les produits (via des objets) et sur les sommes (via des variants polymorphes)
- Des Types algébriques généralisés (probablement une des fonctionnalités, après le langage de modules, qui m'a le plus manqué)
- Des Effets définis par l'utilisateur
- Des sommes ouvertes, ce qui peut largement se simuler avec des objets et de l'héritage ceci-dit
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.
ml
et mli
Sur la séparation entre 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.
mli
Gérer l'encapsulation sans 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
ml
Exprimer l'interface depuis le 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 !