Introduction
Vous l’aurez surement remarqué, écrire des typeclass
peut être assez mécanique et répétitif.
Magnolia est une bibliothèque logicielle qui permet de faire de la dérivation de typeclass
, c'est-à-dire, d’écrire des typeclass
pour nous.
La littérature technique à propos de magnolia n’est pas simple à comprendre.
Dans cet article, nous allons tâcher de comprendre, par étape, comment fonctionne magnolia par la pratique.
https://fr.wikipedia.org/wiki/Chopped_and_screwed
Qu’est-ce qu’on bricole ?
Imaginons un ensemble de case class
assez différentes les unes des autres :
Pour chacune de ces case class
, on souhaite pouvoir écrire le code suivant :
Et obtenir une super sortie épique sur notre terminal :
Approches naïves : Pourquoi utiliser la dérivation de typeclass ?
Afin d’apprécier les bénéfices d’un système de dérivation de typeclass tel que magnolia, intéressons-nous à ce que serait la vie sans magnolia.
Méthode à la mano
On va commencer par écrire une typeclass
qui représente notre comportement.
On veut que nos personnages soient en mesure de pousser un cri et de décrire leurs attributs :
Commençons par implémenter ce comportement pour un Human
et un Dwarf
:
On se rajoute une petite classe implicite afin de faciliter l’écriture de notre comportement :
Et voilà le travail :
Vous pouvez trouver le code de cette implémentation ici
Allez courage, il en reste encore un comme ça… pour le moment
On remarque que le code de shoutingDwarf
et shoutingHuman
est sensiblement le même, bien que l’on ne puisse pas le généraliser “facilement”.
Vous l’aurez compris, on va pouvoir se passer du processus répétitif d’implanter, plus ou moins, la même typeclass
plusieurs fois.
Pour nous permettre d’être plus efficace, on va avoir une approche réflexive, autrement dit que l’on va chercher à écrire un programme capable de s’inspecter lui-même.
Il existe deux types de réflexion :
- la réflexion au
runtime
:- Elle permet d’inspecter les types des objets y compris génériques (après réification)
- D’instancier de nouveau objets
- D’appeler des méthodes d’un objet
- la réflexion au
compiletime
:- Permet de faire de la métaprogrammation, c'est-à-dire du code qui va explorer le code (en particulier pour magnolia, la structure des types) via certaines instructions spéciales et générer du code
C’est la deuxième approche qui nous intéresse.
En effet, on veut que notre compilateur “écrive” les typeclasses
pour nous.
Un des avantages de la réflexion au compiletime
est que le compilateur vérifie que le code généré est correct. À l’instar de la réflexion au runtime qui peut planter… au runtime
C’est là que magnolia intervient
Niveau 1 : Join
Dans un premier temps, nous allons chercher à faire en sorte que le comportement de notre typeclass
soit disponible pour n’importe quelle case class
C’est la fonction join
qui permet de faire ça. On trouve aussi cette fonction sous le nom combine
Ci-dessous, la structure de base d’une dérivation de typeclass
:
Commençons par dériver la méthode shout
:
- On sait que nos
case class
ont généralement un argumentsound
- On voudrait que la sortie ressemble à
SOUND TO UPPER CASE
- Les paramètres de nos
case class
sont accessibles viactx.parameters
Si on essaye de compiler maintenant, voilà ce qu’il risque d’arriver :
Effectivement, à ce stade, magnolia ne sait pas quoi faire des String
et des Int
qui composent nos Race
Pour notre sujet, nous avons juste besoin de renvoyer leur valeur sous forme de String
:
Et ça marche !
Résumé du Niveau 1 :
- Le code du Niveau 1 est disponible ici
- La méthode
join
nous permet de dériver le comportement d’unetypeclass
sur n’importe quellecase class
- On peut agir sur les paramètres de nos
case class
- Il faut décrire le comportement des primitives telles que
String
etInt
afin d’effectuer une dérivation
Comment améliorer le Niveau 1 :
- On se rend compte que c’est un peu bizarre de devoir définir
epicDescription
pour unString
ou unInt
- Il n’est pas très ergonomique d’écrire
shoutingHuman.shout(human)
maishuman.shout
Niveau 2
Dans un premier temps, nous allons commencer par découper notre typeclass
Shout
Les avantages de ce pattern sont multiples.
Dans un premier temps, il nous permet de définir plus facilement la manière dont nos valeurs doivent se comporter
On verra un autre intérêt lorsqu’on s’attaquera au code de join
Aussi, on peut ajouter une implicit class
qui va nous permettre d’écrire human.shout
Intéressons-nous maintenant au code de join
pour epicDescritpion
.
C’est un cas classique, ctx
nous permet d’obtenir le nom des paramètres et le déréférencement de la case class
nous permet d’obtenir les valeurs associées.
Jusqu’ici tout va bien :
Mais imaginons maintenant que l’on décide d’ajouter une nouvelle case classe, composée de deux autres
Par exemple, une petite personne qui serait le produit d’un humain et d’un nain
On peut facilement imaginer que la sortie ne va pas ressembler à notre super formatage avec des sauts de lignes et des tabulations 🥲
Effectivement rien ne va plus et c’est là que notre pattern qui visait à séparer le comportement des valeurs et des cases classes devient pratique :
En faisant du pattern matching sur la typeclass
(disponible dans ctx) on peut avoir des comportements distincts pour chaque type :
Résumé du Niveau 2 :
- Le code du Niveau 2 est disponible ici
- Séparer les comportements des valeurs et des case classes nous permet d’écrire du code plus concis et nous facilite l’écriture de la dérivation.
- La dérivation de
typeclass
ne nous empêche pas de créer une classe implicite
Comment améliorer le Niveau 2 :
- Dans cet exemple, on ne manipule jamais Race directement, il se passe quoi si on fait ça ?
Niveau 3
Imaginons encore une fois une nouvelle case class
composée de deux Race
dont on ne connait pas le type.
À l’image du niveau 2, si on combine notre humain et notre nain, on aimerait avoir le même comportement que précédemment
Sauf que :
On nous demande d’implémenter split
afin de pouvoir dériver des typeclass
pour des sealed trait
, dans notre cas Race
Dans notre cas, l’implantation de split
consiste à :
- Trouver pour une instance a de type
T
, le sous-type deRace
auquelT
correspond - Une fois trouvé, d’invoquer
shout
ouepicDescription
avec l’instance deRace
fournie(icia
), castée dans le bon sous type de Race
Cette implémentation de split
conviendrait pour un autre sealed trait
que Race
mais malheureusement magnolia est limité quand il s’agit de dérivations récursives (des races dans des races).
Pour palier a ce problème, on est obligé de forcer la dérivation de Race
:
Résumé du Niveau 3 :
- Le code du Niveau 3 est disponible ici
- On peut dériver des
typeclass
pour dessealed trait
à l’aide de la méthodesplit
- On est obligé de forcer la dérivation de notre
sealed trait
dans le cas de dérivations récursives.
À suivre
Dans un prochain article, nous regarderons si Scala 3 nous permet de faire plus facilement des dérivations récursives de typeclass
Pour aller plus loin
https://univalence.io/blog/articles/typeclass-derivation-faites-eclore-vos-instances-avec-magnolia/