Déduction automatique d'instance de typeclass

avec shapeless

Couritas Etienne

@courieti

  1. Les types classes
  2. Représentation générique
  3. 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

Made with Slides.com