Cet article est une réinterprétation francophone d'un article publié le blog de Tarides, en Anglais, qui présente l'utilisation de la commande destruct
pour générer des motifs manquants dans du filtrage par motif.
Effective ML, au travers de la commande 'destruct'
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.