Les serveurs Merlin et OCaml
LSP, deux serveurs de langage OCaml
étroitement liés (en effet, OCaml LSP utilise des bibliothèques décrites dans
Merlin), améliorent la productivité grâce à des fonctionnalités telles que
l'auto-complétion et l'inférence de types. Leur commande destruct
, moins
connue mais très utile, simplifie l'utilisation du filtrage par motif en
générant des instructions de correspondance exhaustives, comme nous le
montrerons dans cet article. La commande a récemment bénéficié de quelques
améliorations, la rendant plus utilisable, et nous profitons de cette mise à
jour pour la présenter et illustrer quelques cas d'utilisation.
Un bon IDE pour un langage de programmation doit fournir des informations
contextuelles, telles que des suggestions de complétion, des détails sur les
expressions comme les types, et des retours d'erreurs en temps réel. Cependant,
dans un monde idéal, il devrait également servir d'assistant à l'écriture de
code, capable de générer du code selon les besoins. Et bien qu'il existe
indéniablement des points communs entre un large éventail de langages de
programmation, permettant la "généralisation" des interactions avec un éditeur
de code via un protocole (tel que LSP),
certains langages possèdent des fonctionnalités rares ou même uniques qui
nécessitent un traitement spécial. Heureusement, il est possible de développer
des fonctionnalités adaptées à ces particularités. Celles-ci peuvent être
invoquées dans LSP via des requêtes personnalisées pour récupérer des
informations arbitraires et des actions de code pour transformer un document
selon les besoins. Splendide ! Cependant, une telle fonctionnalité peut être
plus difficile à découvrir, car elle dénormalise quelque peu l'expérience
utilisateur de l'IDE. C'est le cas de la commande destruct
, qui est
immensément utile et fait gagner beaucoup de temps.
Dans cet article, nous allons tenter de comprendre l'utilité de cette commande
et son application à l'aide d'exemples quelque peu simplistes. Ensuite, nous
approfondirons avec quelques exemples moins artificiels que j'utilise dans mon
codage quotidien. J'espère que cet article sera utile et divertissant tant pour
les personnes qui connaissent déjà destruct
que pour celles qui ne le
connaissent pas.
destruct
, dans les grandes lignes
OCaml permet l'expression de types de données
algébriques qui, associés au filtrage
par motif, peuvent être utilisés pour
décrire des structures de données et effectuer une analyse de cas. Dans le cas
où un filtrage par motif n'est pas exhaustif, l'avertissement 8, connu sous
le nom de partial-match
, sera levé lors de la phase de compilation. Il est
donc conseillé de maintenir des blocs de filtrage exhaustifs.
Pour ceux qui ne connaissent pas le terme
destruct
, le filtrage par motif est une analyse de cas, et l'expression de la forme (un ensemble de motifs) sur laquelle vous effectuez le filtrage est appelée déstructuration, car vous déballez des valeurs à partir de données structurées. C'est la même terminologie utilisée en JavaScript.
La commande destruct
aide à atteindre cette exhaustivité. Lorsqu'elle est
appliquée à un motif (via M-x merlin-destruct
dans Emacs, :MerlinDestruct
dans Vim, et Alt + d
dans Visual Studio Code), elle génère des motifs. La
commande se comporte différemment selon le contexte du curseur :
-
Lorsqu'elle est appelée sur une expression, elle la remplace par un filtrage par motif sur ses constructeurs.
-
Lorsqu'elle est appelée sur un motif d'un filtrage non exhaustif, elle rend le filtrage exhaustif en ajoutant les cas manquants.
-
Lorsqu'elle est appelée sur un motif générique (wildcard), elle le précisera si possible.
Examinons chacun de ces scénarios à l'aide d'exemples. J'utilise Doom Emacs, cependant, l'ensemble des exemples devraient être parfaitement adaptables dans votre éditeur favori !
Déstructuration sur une expression
La déstructuration d'une expression fonctionne de manière assez évidente. Si le vérificateur de types connaît le type de l'expression (dans notre exemple, il le connaît par inférence), l'expression sera remplacée par un filtrage sur tous les cas énumérables.
Déstructuration sur une correspondance de motifs non-exhaustive
Le second comportement est, à mon avis, le plus pratique. Bien que j'aie
rarement besoin de remplacer une expression par un filtrage par motif, je veux
souvent effectuer une analyse de cas sur tous les constructeurs d'un type somme.
En implémentant juste un motif unique, comme Foo
, mon expression de filtrage
n'est pas exhaustive, et si je fais une destruct
sur celui-ci, elle générera
tous les cas manquants.
Déstructuration sur un motif générique
Le comportement final est très similaire au précédent ; lorsque vous appliquez
destruct
à un motif générique (ou à un motif produisant un générique, par
exemple, une déclaration de variable), la commande générera toutes les branches
manquantes.
Déstructuration en présence de nesting
Lorsqu'elle est utilisée de manière interactive, il est possible de déstructurer
des motifs imbriqués pour atteindre rapidement l'exhaustivité. Par exemple,
imaginons que notre variable x
soit de type t option
:
-
Nous commençons par déstructurer notre motif générique (
_
), ce qui produira deux branches,None
etSome _
. -
Ensuite, nous pouvons appliquer
destruct
sur le motif générique associé deSome _
, ce qui produira tous les cas concevables pour le typet
.
Dans le cas de produits (plutôt que de sommes)
Dans les exemples précédents, nous avons toujours traité des cas dont les
domaines sont parfaitement définis, ne déstructurant que des cas de branches de
types somme simples. Cependant, la commande destruct
peut également agir sur
des produits. Considérons un exemple très ambitieux où nous effectuerons un
filtrage par motif exhaustif sur une valeur de type t * t option
, générant
tous les cas possibles en utilisant uniquement destruct
:
Il est possible de constater que lorsqu'il est utilisé de manière interactive, la commande permet de gagner beaucoup de temps, et associée aux retours en temps réel de Merlin concernant les erreurs, on peut rapidement déterminer si notre filtrage par motif est exhaustif. Dans un sens, c'est un peu comme un "dérivateur" manuel.
La commande destruct
peut agir sur n'importe quel motif, elle fonctionne donc
également dans les arguments de fonction (bien que leur représentation ait
légèrement changé pour la version 5.2.0
), et en plus de déstructurer les tuples,
il est également possible de déstructurer des enregistrements, ce qui peut être
très utile pour notre quête d'exhaustivité !
Quand l'ensemble des constructeurs est non-fini
Parfois, les types ne sont pas énumérables de manière finie. Par exemple,
comment pouvons-nous gérer les chaînes de caractères ou même les entiers ? Dans
de telles situations, destruct
tentera de trouver un exemple. Pour les
entiers, ce sera 0
, et pour les chaînes de caractères, ce sera la chaîne vide.
Excellent ! Nous avons couvert une grande partie des comportements de la
commande destruct
, qui sont très pertinents dans leur contexte. Il y en a
d'autres (comme les cas de destruction en présence de GADTs qui ne génèrent que
des sous-ensembles de motifs), mais il est temps de passer à un exemple du monde
réel !
La quête de l'exhaustivité: Effective ML
En 2010, Yaron Minsky a donné une excellente présentation sur les raisons (et les avantages) d'utiliser OCaml chez Jane Street. En plus d'être très inspirante, elle fournit des insights spécifiques et des pièges à éviter pour utiliser OCaml efficacement dans un contexte industriel incroyablement sensible (d'où le nom "Effective ML"). C'est dans cette présentation que la maxime "Rendre les états illégaux non représentables" a été mentionnée publiquement pour la première fois, une phrase qui serait plus tard fréquemment utilisée pour promouvoir d'autres technologies (comme Elm). De plus, la présentation anticipe de nombreuses discussions sur la modélisation de domaine, chères à la communauté du Software Craftsmanship, en proposant des stratégies de réduction de domaine (plus tard largement développées dans le livre Domain Modeling Made Functional).
Parmi la liste des approches efficaces pour utiliser un langage ML, Yaron
présente un scénario où l'on pourrait trop rapidement utiliser le générique dans
une analyse de cas. L'exemple est étroitement lié à la finance, mais il est
facile de le transposer dans un exemple plus simple. Nous allons implémenter une
fonction equal
pour un type très basique :
type t =
| Foo
| Bar
La fonction equal
peut être implémentée trivialement comme suit :
let equal a b =
match (a, b) with
| Foo, Foo -> true
| Bar, Bar -> true
| _ -> false
Notre fonction fonctionne parfaitement et est exhaustive. Cependant, que se
passe-t-il si nous ajoutons un constructeur à notre type t
?
type t
| Foo
| Bar
+ | Baz
Notre fonction, dans le cas de equal Baz Baz
, renverra false
, ce qui n'est
évidemment pas le comportement attendu. Comme le générique rend notre fonction
exhaustive, le compilateur ne signalera aucune erreur. C'est pourquoi Yaron
Minsky soutient que dans de nombreux cas avec une clause générique, c'est
probablement une erreur. Si notre fonction avait été exhaustive, l'ajout d'un
constructeur aurait levé un avertissement partial-match
, nous obligeant à
décider explicitement comment nous comporter en présence du nouveau constructeur
! Par conséquent, utiliser un wildcard dans ce contexte nous prive de la
refactorisation sans crainte, qui est une force d'OCaml. C'est en effet un
argument en faveur de l'utilisation d'un préprocesseur pour générer des
fonctions d'égalité, en utilisant, par exemple, le dériveur standard
eq
ou le plus hygiénique
Ppx_compare
. Mais parfois,
utiliser un préprocesseur n'est pas possible. Heureusement, la commande
destruct
peut nous aider à définir une fonction d'égalité exhaustive !
Nous allons procéder étape par étape, en séparant spécifiquement les différents cas et en utilisant un filtrage par motif imbriqué pour rendre les différents cas faciles à exprimer de manière récurrente :
Comme nous pouvons le voir, destruct
nous permet de mettre en œuvre rapidement
une fonction equal
exhaustive sans utiliser de motifs génériques.
Maintenant, nous pouvons ajouter notre constructeur Baz
pour voir comment se
déroule la refactoring ! En ajoutant un constructeur, nous détectons rapidement
un motif récurrent où nous essayons de donner à la commande destruct
autant
de liberté que possible pour générer les motifs manquants !
Fantastique ! Nous avons pu mettre en œuvre rapidement une fonction equal
.
Ajouter un nouveau cas est trivial, laissant destruct
gérer tout le travail !
Associée à des fonctionnalités modernes d'édition de texte (par exemple,
l'utilisation de multi-curseurs), il est possible de gagner énormément de temps
! Un autre exemple de l'utilisation immodérée de destruct
(mais trop long pour
être détaillé dans cet article) était l'implémentation du module
Mime dans
YOCaml pour générer des flux RSS.
En conclusion
Associé à un formateur comme
OCamlFormat (pour reformater
proprement les fragments de code générés), destruct
est un outil peu
conventionnel dans le paysage des IDE. Il s'aligne avec les types algébriques et
le filtrage par motif pour simplifier l'écriture du code et progresser vers un
code plus facile à refactoriser et donc à maintenir ! Consciente de l'utilité de
la commande, l'équipe de Merlin continue de
la maintenir, en optimisant les dernières fonctionnalités d'OCaml pour rendre la
commande aussi utilisable que possible dans le plus de contextes possible !
J'espère que cette collection d'exemples illustrés vous a motivé à utiliser la
fonction destruct
si vous n'en aviez pas déjà connaissance. N'hésitez pas à
nous envoyer des idées d'améliorations, de correctifs et de cas d'utilisation
amusants via X ou
LinkedIn !
Bon hacking.
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!