Déduction automatique d'instance de typeclass
avec shapeless
Couritas Etienne
@courieti
- Les types classes
- Représentation générique
- Déduction automatique d'instances
par Dave Gurnell
@davegurnell
Un guide
Il y a même une traduction française !
Pourquoi voudrait-on faire de la programmation générique?
- Faire du code qui ne dépend pas de types précis
- Éviter les répétitions
- Pour pouvoir se reposer encore plus sur les types
Les types classes Quésaco ?
Le principe
Séparer les fonctionalités de la donnée
- Les Writer[T]/Reader[T] de PlayJson
- Les Encoder[T]/Decoder[T]
- Comparator<T>
- Numeric[T]
- Ordering[T]
QUELQUES EXEMPLES
Notre interface d'ordering
trait Ordering[T] {
def compare(x: T, y: T): Int
}
Utilisation de notre type classe
def prettyCompare[T](x : T, y : T)
(implicit ordering : Ordering[T]) = {
ordering.compare(x,y) match {
case 0 => "x est égal à y"
case i if i > 0 => "x est plus grand que y"
case i if i < 0 => "x est plus petit que y"
}
}
Implémentation pour Int
implicit val intOrdering : Ordering[Int] =
new Ordering[Int] {
def compare(x: Int, y: Int): Int = {
Integer.compare(x, y)
}
}
Comment faire pour ne pas avoir à écrire
soit-même chaque type classe ?
Shapeless
Les types de données algébriques ou ADTs
sealed trait Forme
final case class Rectangle(largeur: Double, hauteur: Double) extends Forme
final case class Cercle(rayon: Double) extends Forme
Représentation générique d'un product
La HList
Une simple liste de types
mais il s'agit d'un type et non d'une instance
HList
La HList
Soit la liste est vide :
Soit la liste a un élément et une queue :
Head est un type abitraire
HNil
::[A,B <: HList]
Représentation générique de la case class IceCream
case class IceCream(name: String, numCherries: Int, inCone: Boolean)
String :: Int :: Boolean :: HNil
Utilisation de shapeless.Generic avec un product
import shapeless.Generic
val iceCreamGen = Generic[IceCream]
val iceCream = IceCream("Sunday", 1, false)
val repr = iceCreamGen.to(myIceCream)
//repr : iceCreamGen.Repr = Sunday :: 1 :: false :: HNil
val iceCream2 = iceCreamGen.from(repr)
// iceCream2 : IceCream(Sunday, 1, false)
Représentation générique d'un coproduit ou familles scellées
sealed trait Forme
final case class Rectangle(largeur: Double, hauteur: Double) extends Forme
final case class Cercle(rayon: Double) extends Forme
Rectangle :+: Cercle :+: CNil
Utilisation de shapeless.Generic avec un coproduct
import shapeless.Generic
val gen = Generic[Forme]
val rect = gen.to(Rectangle(1.0,2.0))
// rect : gen.Repr = Inl(Rectangle(1.0, 2.0))
val circle = gen.to(Cercle(1.0))
// circle : gen.Repr = Inr(Inl(Cercle(1.0)))
La déduction automatique
Revenons à notre type classe
trait Ordering[T] {
def compare(x: T, y: T): Int
}
Commençons par definir quelques outils
object Ordering {
import shapeless.{::, HList, HNil}
//summoner method
def apply[A](implicit ordering: Ordering[A]) = ordering
def createOrdering[T](f : (T,T) => Int) =
new Ordering[T] {
def compare(a: T, b: T) : Int = f(a,b)
}
implicit class OrderingOps[T](a : T)(implicit ord : Ordering[T]){
def compare(b: T) : Int = ord.compare(a,b)
}
}
La définition des types primitifs
implicit val intOrdering : Ordering[Int] =
createOrdering((a,b) => a.compare(b))
implicit val doubleOrdering : Ordering[Double] =
createOrdering((a,b) => a.compare(b))
implicit val stringOrdering : Ordering[String] =
createOrdering((a,b) => a.compare(b))
implicit val booleanOrdering : Ordering[Boolean] =
createOrdering((a,b) => a.compare(b))
L'instance d'ordering de HNil
implicit val hNilOrdering : Ordering[HNil] =
createOrdering((a,b) => 0)
Implicit Scope
Ordering[ Int ]
Ordering[ String ]
Ordering[ Boolean ]
Ordering[ Double ]
Ordering[ HNil ]
Ordering[ H :: T ]
Ordering[String :: Int :: Boolean :: HNil]
Ordering[String]
Ordering[Int :: Boolean :: HNil]
Ordering[String]
Ordering[Boolean :: HNil]
Ordering[Int]
Ordering[String]
Ordering[Int]
Ordering[Boolean]
Ordering[HNil]
L'instance d'ordering pour HList
implicit def hListOrdering[H, T <: HList](
implicit
hOrdering: Ordering[H],
tOrdering: Ordering[T]
) : Ordering[H :: T] =
createOrdering((a,b) => {
hOrdering.compare(a.head,b.head) match {
case 0 => tOrdering.compare(a.tail,b.tail)
case r => r
}
})
Lazy
implicit divergence
sealed trait Tree[T]
final case class Branch[T]( left : Tree[T], right: Tree[T]) exends Tree[T]
final case class Leaf[T]( value : T ) exends Tree[T]
//1
Ordering[Tree[Int]]
//2
Ordering[Branch[Int] :+: Leaf[Int] :+: Cnil]
//3
Ordering[Branch[Int]]
//4
Ordering[Tree[Int] :: Tree[Int] :: HNil]
//5
Ordering[Tree[Int]] //Outch!
L'instance d'ordering pour HList avec lazy
implicit def hListOrdering[H, T <: HList](
implicit
hOrdering: Lazy[Ordering[H]],
tOrdering: Ordering[T]
) : Ordering[H :: T] =
createOrdering((a,b) => {
hOrdering.value.compare(a.head,b.head) match {
case 0 => tOrdering.compare(a.tail,b.tail)
case r => r
}
})
Maintenant comment convertir nos case classes en HList
implicit def genericOrdering[T](
implicit
gen : Lazy[Generic[T]],
ordering : Ordering[gen.Repr]): Ordering[T] =
{
createOrdering((a: T,b: T) =>
ordering.value.compare(gen.to(a), gen.to(b))
)
}
Generic[T] contien un membre Repr qui est la représentation générique de T
en somme gen.Repr est une HList
Ce que l'on veut c'est un Ordering[gen.Repr]
implicit def genericOrdering[T](
implicit
gen : Generic[T],
ordering : Ordering[gen.Repr]
): Ordering[T]
Malheureusement le compilateur ne nous permet pas d'accéder à un membre de type d'un autre paramètre.
Mais il y a une solution.
implicit def genericOrdering[T, H <: HList](
implicit
gen : Generic[T]{ type Repr = H},
ordering : Ordering[H]
): Ordering[T]
On peut faire l'analogie avec une fonction
Generic[T]{ type Repr = H}
T => H
est un alias de
Generic[T]{ type Repr = H}
Generic.Aux[T, H]
On peut imaginer comparer des éléments d'une famille scellée entre eux. Pour l'exemple on utilisera juste l'ordre de déclaration.
Comme pour HList la définition de l'élément est très simple.
implicit def cNilOrdering: Ordering[CNil] =
createOrdering((a,b) =>
throw new Exception("imposiburu!")
)
/!\ Il n'y a pas d'instance de CNil donc ce code ne sera jamais éxécuté.
Il est juste là pour aider à la compilation.
L'instance pour un Ordering de coproduct
implicit def coproductOrdering[H, T <: Coproduct] (
implicit
hOrdering : Lazy[Ordering[H]],
tOrdering : Ordering[T]
): Ordering[H :+: T] =
createOrdering ((a,b) =>
(a,b) match {
case (Inl(a), Inl(b)) => hOrdering.value.compare(a,b)
case (Inr(a), Inr(b)) => tOrdering.compare(a,b)
case (Inr(a), Inl(b)) => 1
case (Inl(a), Inr(b)) => -1
}
)
Traitement similaire aux HList
Utilisation
scala> val a : Forme = Cercle(2.0)
a: Forme = Cercle(2.0)
scala> val b : Forme = Cercle(1.0)
b: Forme = Cercle(1.0)
scala> a.compare(b)
res0: Int = 1
sealed trait Forme
final case class Rectangle(largeur: Double, hauteur: Double) extends Forme
final case class Cercle(rayon: Double)extends Forme
scala> val c: Forme = Rectangle(1.0, 2.0)
c: Forme = Rectangle(1.0,2.0)
scala> c.compare(a)
res3: Int = 1
scala> c.compare(c)
res4: Int = 0
LabelledGenerics
Produit des HList de FieldType
Parfois défini de cette façon : "width" ->> Double
La version simplifiée du résultat dans la console
scala> LabelledGeneric[Rectangle]
res10: shapeless.LabelledGeneric[Rectangle]{
type Repr = "largeur" ->> Double :: "hauteur" ->> Double :: HNil
} = shapeless.LabelledGeneric$$anon$1@6306fc65
Shapeless Ops.
Défini dans le package shapeless.ops
C'est un ensemble de type classes prédéfinies, « Fonctions à types dépendants »
Pour HList cela ressemble à l'api des iterables
- Last
- ToList
- Union
- Size
Utilisation récursive d'`Implicitly` à la recherche de la typeclass manquante
Utiliser `reify`, s'il y a encore des types génériques ou des Any/Nothing, c'est qu'il y a un problème
Développer avec shapeless
Aller plus loin
- Lire type astronaute guide to shapeless
- Lire le code shapeless
- Liste de ressources lunatech
Questions ?
@courieti
https://github.com/crakjie/shapeless-guide
Déduction automatique d'instance de typeclass avec shapeless
By Etienne Couritas
Déduction automatique d'instance de typeclass avec shapeless
- 1,446