Dans un précédent article, nous avions parlé de dérivation de typeclass en Scala 2 avec Magnolia. Nous avions vu qu’il est possible de dériver des typeclass
pour des sealed trait
à l’aide de la méthode split
. En revanche, nous avons aussi observé que dans les cas de dérivation récursive, il nous fallait forcer la dérivation de notre sealed trait
.
Dans cet article, nous allons tâcher de montrer comment faire une dérivation récursive de typeclass avec magnolia en Scala 3.
Reprenons…
Imaginons que l’on ait le co-produit suivant :
sealed trait A
case object B(str: String) extends A
case class C(next: A) extends A
On souhaite dériver la typeclass Show
suivante pour toute instance de A
:
trait Show[T] {
def show(t: T): String
}
trait ShowObj[T] extends Show[T] {
def showObj(t: T): String
}
On peut se demander pourquoi nous avons Show
et ShowObj
?
Ce pattern peut être utile lorsque l’on ne souhaite pas implémenter des comportements spécifiques à un type.
Par exemple, on peut imaginer que l’on ait besoin de Show
pour afficher les types primitifs sur la console (String
,Int
…) et ShowObj
pour afficher les case class
Unsupported block 2e2d5288-2960-45c8-9bcc-ef822fbcd276 unsupported
in https://www.notion.so/D-rivation-r-cursive-de-typeclass-avec-Scala-3-bb1ec3911d08498ca4514f53d0cf00f0
Dans notre exemple, on va essayer de dériver Show de manière à obtenir :
Show[String]
Show[Int]
ShowObj[CaseClassOne]
ShowObj[CaseClassTwo]
Maintenant, projetons notre exemple sur notre Trait A
On remarque que l’on va souhaiter obtenir les dérivations suivantes :
Show[String]
ShowObj[A]
→ va être obtenu par la méthode splitShowObj[B]
ShowObj[C]
On peut voir avec la figure 3 que Show[C]
va être récursive.
En effet, C
contient un A
qui peut être un C
qui contient un A
…
Nous avons vu qu’avec Magnolia en Scala 2, la dérivation récursive de typeclass semble avoir des bugs. Partons d’un exemple rapide proche de ce que nous voulons faire.
trait Tc[T]
trait TcObj[T] extends Tc[T]
object ShowDerivation {
type Typeclass[T] = Tc[T]
def join[T](ctx: CaseClass[Tc, T]): TcObj[T] = new TcObj[T] {}
def split[T](ctx: SealedTrait[Tc, T]): TcObj[T] = new TcObj[T] {}
def gen[T]: Tc[T] = macro Magnolia.gen[T]
def genObj[T]: TcObj[T] = macro Magnolia.gen[T]
}
Voici l’utilisation de ce code.
ShowDerivation.gen[B.type] // success ☑️
ShowDerivation.genObj[B.type] // success ☑️
ShowDerivation.gen[A] // success ☑️
//ShowDerivation.genObj[A] // failure ❌
/*
type mismatch;
found : Playground.ShowDerivation.Typeclass[Playground.A]
(which expands to) Playground.Tc[Playground.A]
required: Playground.TcObj[Playground.A]
*/
implicit val tca:Tc[A] = null
ShowDerivation.gen[C] // success ☑️
ShowDerivation.genObj[C] // success ☑️
Qu’en est-il en Scala 3 ?
On est toujours obligé de forcer la dérivation. La documentation de Magnolia l’explique ici : https://github.com/softwaremill/magnolia#limitations.
Un exemple
Dans cet exemple, l’idée est de reprendre le problème de la figure 2
, mais en Scala 3.
On va tâcher de dériver la typeclass Show
pour le trait suivant :
sealed trait Kind
case class Human(name: String) extends Kind
case class Elf(elvishName: String) extends Kind
case class Hybrid(kind: Kind, other: Kind) extends Kind
Le type Kind
représente donc soit des humains, soit des elfes, soit des êtres hybrides (avec deux noms ^^).
Reprenons le code de la figure 4 pour le porter en Scala 3 :
object Show extends Derivation[Show]{
override def join[T](ctx: CaseClass[Show, T]): ShowObj[T] = new ShowObj[T] {???}
override def split[T](ctx: SealedTrait[Show, T]): ShowObj[T] = new ShowObj[T] {???}
inline given autoDerived[A](using Mirror.Of[A]): Show[A] =
derived.asInstanceOf[Show[A]]
inline given derivedObj[A](using Mirror.Of[A]): ShowObj[A] =
derived.asInstanceOf[ShowObj[A]]
}
Comme on l’a vu dans la Figure 3 nous allons avoir besoin de Show[String]
pour afficher les String
sur la console
given Show[String] with
def show(t: String): String = s"$t"
Désormais, nous avons un moyen d’afficher nos String
et le squelette principal de notre dérivation.
Il nous faut alors implémenter split
et join
qui vont nous permettre de dériver les typeclass pour les sealed trait et les case class :
override def join[T](ctx: CaseClass[Show, T]): ShowObj[T] = new ShowObj[T] {
override def show(t: T): String = t.toString.toUpperCase()
override def showObj(t: T): String =
ctx.params.map(param => param.typeclass match {
case showObj: ShowObj[param.PType] =>
s"${param.label} part: [ ${showObj.showObj(param.deref(t))} ] "
case show: Show[param.PType] =>
s"${param.label}: [${show.show(param.deref(t))}]"
}).mkString
}
override def split[T](ctx: SealedTrait[Show, T]): ShowObj[T] = new ShowObj[T] {
override def show(t: T): String = ctx.choose(t) { sub =>
sub.typeclass.show(sub.cast(t))
}
override def showObj(t: T): String = ctx.choose(t) { sub =>
val tc = sub.typeclass.asInstanceOf[ShowObj[T]]
tc.showObj(sub.cast(t))
}
}
Arrêtons-nous sur deux choses :
- Le
showObj
dejoin
:- On regarde si on souhaite dériver un
Show
ou unShowObj
de notre case class (en l’occurrence, on voudra unShow
, si on a un type primitif, ou unShowObj
, si on a une case class). - On formate correctement les valeurs des paramètres en les déréférençant, c'est-à-dire en récupérant leur valeur “réelle”.
- On regarde si on souhaite dériver un
- Le
showObj
desplit
:- On parcourt tous les sous-types d’un sealed trait (en l’occurrence, les sous-types de
Kind
) pour trouver celui que l’on cherche à dériver. - On cast notre sous-type dans son type réel (eg. j’ai un sous-type de
Kind
, si je vois que son type réel estElf
, alors je le cast enElf
) et on appelleshowObj
pour ce bon type.
- On parcourt tous les sous-types d’un sealed trait (en l’occurrence, les sous-types de
Utilisons tout cela désormais.
val aelfraed: Kind = Elf("Aelfraed")
val maeva: Kind = Human("Maeva")
val kind :Kind = Hybrid(aelfraed, maeva)
given instance2: Show[Kind] = Show.derived
val showKind: Show[Kind] = summon[Show[Kind]]
println(s"1 - ${showKind.show(kind)}")
given instance3: ShowObj[Kind] = Show.derivedObj
val showObjKind: ShowObj[Kind] = summon[ShowObj[Kind]]
println(s"2 - ${showObjKind.showObj(kind)}")
Ce qui donne.
1 - HYBRID(ELF(AELFRAED),HUMAN(MAEVA))
2 - kind part: [ elvishName: [Aelfraed] ] other part: [ name: [Maeva] ]
Si on retourne sur la Figure 3 on voit que l’on a bien obtenu :
Show[String]
ShowObj[A]
→ obtenu par la méthode splitShowObj[B]
ShowObj[C]
Et que Show[T]
peut aussi dériver un A
(cf. HYBRID(ELF(AELFRAED),HUMAN(MAEVA))
) comme indiqué dans la Figure 2.
Voilà le code exécutable dans une worksheet, happy hacking!
import language.experimental.macros
import magnolia1.*
import scala.deriving.Mirror
trait Show[T] {
def show(t: T): String
}
trait ShowObj[T] extends Show[T] {
def showObj(t: T): String
}
object Show extends Derivation[Show]{
given Show[String] with
def show(t: String): String = s"$t"
given Show[Int] with
def show(t: Int): String = s"#${t.toString} -"
override def join[T](ctx: CaseClass[Show, T]): ShowObj[T] = new ShowObj[T] {
override def show(t: T): String = t.toString.toUpperCase()
override def showObj(t: T): String = ctx.params.map(param => param.typeclass match {
case showObj: ShowObj[param.PType] => s"${param.label} part: [ ${showObj.showObj(param.deref(t))} ] "
case show: Show[param.PType] => s"${param.label}: [${show.show(param.deref(t))}]"
}).mkString
}
override def split[T](ctx: SealedTrait[Show, T]): ShowObj[T] = new ShowObj[T] {
override def show(t: T): String = ctx.choose(t) { sub =>
sub.typeclass.show(sub.cast(t))
}
override def showObj(t: T): String = ctx.choose(t) { sub =>
val tc = sub.typeclass.asInstanceOf[ShowObj[T]]
tc.showObj(sub.cast(t))
}
}
inline given autoDerived[A](using Mirror.Of[A]): Show[A] = derived.asInstanceOf[Show[A]]
inline given derivedObj[A](using Mirror.Of[A]): ShowObj[A] = derived.asInstanceOf[ShowObj[A]]
}
Et voici l’utilisation.
import Show.given_Show_String
sealed trait Root
case object B extends Root
case class C(next:Root) extends Root
given instance: Show[Root] = Show.derived
val showA: Show[Root] = summon[Show[Root]]
val root: Root = C(C(B))
println(showA.show(root))
sealed trait Kind
case class Human(name: String) extends Kind
case class Elf(elvishName: String) extends Kind
case class Hybrid(kind: Kind, other: Kind) extends Kind
val aelfraed: Kind = Elf("Aelfraed")
val maeva: Kind = Human("Maeva")
val kind :Kind = Hybrid(aelfraed, maeva)
given instance2: Show[Kind] = Show.derived
val showKind: Show[Kind] = summon[Show[Kind]]
println(showKind.show(kind))
given instance3: ShowObj[Kind] = Show.derivedObj
val showObjKind: ShowObj[Kind] = summon[ShowObj[Kind]]
println(showObjKind.showObj(kind))
Ressources
code de l’article : https://github.com/univalence/recursive-derivation-magnolia/blob/main/src/main/scala/ShowDerivationMagnolia3.scala