2. SOMMAIRESOMMAIRE
Moi, moi, moi !
Relation donnée / comportement: divergence de points de vue
Polymorquoi ?
Anatomie de la type class
Type class meca-augmentée !
La boite à outils
Aller plus haut !
3. MOI, MOI, MOI !MOI, MOI, MOI !
Data ingénieur Ebiznext
ESN avec une forte expertise autour de la data / Scala
Animateur du pôle data
En charge de la branche nantaise
Co-organisateur du SNUG
Passionné de FP
@mmenestret
geekocephale.com
4. RELATION DONNÉE / COMPORTEMENT:RELATION DONNÉE / COMPORTEMENT:
DIVERGENCE DE POINTS DE VUE !DIVERGENCE DE POINTS DE VUE !
5. SÉPARATION DONNÉE / COMPORTEMENTSÉPARATION DONNÉE / COMPORTEMENT
La programmation orientée objet et la programmation fonctionnelle: deux approches opposées !
6. ORIENTÉ OBJETORIENTÉ OBJET
L'OOP combine la donnée et les comportements au sein de classes
Encapsule et cache la donnée dans un état interne
Expose les comportements sous forme de méthodes pour agir sur celui-ci
final class Player(private val name: String, private var level: Int) {
def levelUp(): Unit = { level = level + 1 }
def sayHi(): String = s"Hi, I'm player $name, I'm lvl $level !"
}
7. PROGRAMMATION FONCTIONNELLEPROGRAMMATION FONCTIONNELLE
La FP sépare complètement la donnée des comportements
La donnée est modelisée par des types algébriques de donnée (ADTs)
Les comportement sont modélisés par des fonctions (depuis et vers ces types)
final case class Player(name: String, level: Int)
object PlayerOperations {
def levelUp(p: Player): Player = p.copy(level = p.level + 1)
def sayHi(p: Player): String = s"Hi, I'm player ${p.name}, I'm lvl ${p.level} !"
}
8. EXPRESSION PROBLEMEXPRESSION PROBLEM
Comment se comporte un langage ou un paradigme quand on:
Étend un type existant (ajouter des "cas" à un type)
Personnage = Joueur + Personnage non joueur
Et si on ajoute Boss ?
Étend les comportements d'un type existant
Un personnage peut dire bonjour et monter en niveau
Et si on ajoute le comportement de se déplacer ?
Ces actions entrainent-elles des modifications de la code base existante ?
9. ORIENTÉ OBJETORIENTÉ OBJET
: Étendre un type existant
Juste une nouvelle classe qui extends mon type à étendre (la code base d'origine reste inchangée)
: Étendre les comportements d'un type existant
Nouvelle méthode sur le type dont on veut étendre le comportement
Impact sur tous ses sous types pour y implémenter cette nouvelle méthode...
10. PROGRAMMATION FONCTIONNELLEPROGRAMMATION FONCTIONNELLE
: Étendre un type existant
Nouvelle implémentation du trait représentant le type à étendre
Impact sur toutes les fonctions existantes prenant ce type en paramètre pour traiter cette nouvelle
implémentation...
: Étendre les comportements d'un type existant
Juste une nouvelle fonction (la code base d'origine reste inchangée)
12. DEFINITIONDEFINITION
Mécanisme visant à augmenter la réutilisation de code grâce à des constructions plus génériques.
Il y a plusieurs types de polymorphisme.
14. POLYMORPHISME D'HÉRITAGEPOLYMORPHISME D'HÉRITAGE
Plusieurs classes héritent leurs comportements d'une super classe commune.
class Character(private val name: String) {
def sayHi(): String = s"Hi, I'm $name"
}
class Player(private val name: String, private var level: Int) extends Character(name) {
def levelUp(): Unit = { level = level + 1 }
}
15. POLYMORPHISME AD HOCPOLYMORPHISME AD HOC
Une fonction se réfère à une "interface" commune à un ensemble de types arbitraires.
Cette "interface" abstrait un ou plusieurs comportements communs à ces types
Evite de ré-implémenter une fonction pour chaque type concrets
Son comportement dépendra du type concret de son / ses paramètre(s)
On va s'intéresser à celui-ci !
16. DEUX IMPLÉMENTATIONS DU POLYMORPHISME AD HOCDEUX IMPLÉMENTATIONS DU POLYMORPHISME AD HOC
Interface subtyping / adapter pattern - ಥ_ಥ
def show(s: Showable): String
Type classes - ᕕ( ᐛ )ᕗ
def show[S: Showable](s: S): String
18. OBSERVATIONSOBSERVATIONS
Construction introduite en Haskell par Philip Wadler
Représente un groupe ou une "classe" de types (type class) arbitraire qui partagent des propriétés
communes
Par exemple:
Le groupe de ceux qui peuvent dire "bonjour"
Le groupe de ceux qui ont des pétales
19. ANATOMIE COMPARÉEANATOMIE COMPARÉE
Joue le même rôle qu'une interface en OOP, MAIS:
Permet d'ajouter des propriétés à des types existant à posteriori
Permet d'encoder une interface conditionnelle
20. ANATOMIE FONCTIONNELLEANATOMIE FONCTIONNELLE
En Scala, on encode les type classes, ce n'est pas une construction de première classe du langage mais un
design pattern (ce n'est pas le cas de tous les langages...).
On l'implémente grâce à:
1. Un trait avec un paramètre de type qui expose les propriétés qui sont abstraites par la type class
2. Les implémentations concrètes de ce trait
21. ETUDE D'UN SPÉCIMENETUDE D'UN SPÉCIMEN
// Notre classe "métier"
final case class Player(name: String, level: Int)
val geekocephale = Player("Geekocephale", 42)
// 1. Un trait: tous les T qui peuvent dire bonjour
trait CanSayHi[T] {
def sayHi(t: T): String
}
// 2. Une implémentation concrète pour que Player soit une instance de CanSayHi
val playerGreeter: CanSayHi[Player] = new CanSayHi[Player] {
def sayHi(t: Player): String = s"Hi, I'm player ${t.name}, I'm lvl ${t.level} !"
}
// Une fonction polymorphique
def greet[T](t: T, greeter: CanSayHi[T]): String = greeter.sayHi(t)
scala> greet(geekocephale, playerGreeter)
res4: String = Hi, I'm player Geekocephale, I'm lvl 42 !
22. ANATOMIE FONCTIONNELLEANATOMIE FONCTIONNELLE
Utilisons les implicits pour se rapprocher de ce qui est fait en Haskell
def greet[T](t: T)(implicit greeter: CanSayHi[T]): String = greeter.sayHi(t)
implicit val playerGreeter: CanSayHi[Player] = new CanSayHi[Player] {
def sayHi(t: Player): String = s"Hi, I'm player ${t.name}, I'm lvl ${t.level} !"
}
scala> greet(geekocephale)
res5: String = Hi, I'm player Geekocephale, I'm lvl 42 !
23. NOTA BENENOTA BENE
2 règles d'hygiène fondamentales:
Une seule implémentation d'une type class par type
On ne met les instances de type class que:
Dans l'object compagnon de la type class
Dans l'object compagnon du type
24. RETOUR À L'ANATOMIE COMPARÉERETOUR À L'ANATOMIE COMPARÉE
AJOUT DE PROPRIÉTÉS À DES TYPES EXISTANTSAJOUT DE PROPRIÉTÉS À DES TYPES EXISTANTS
Maintenant votre URL sait dire bonjour !
import java.net.URL
implicit val urlGreeter: CanSayHi[URL] = new CanSayHi[URL] {
override def sayHi(t: URL): String = s"Hi, I'm an URL pointing at ${t.getHost}"
}
scala> greet(new URL("http://geekocephale.com"))
res6: String = Hi, I'm an URL pointing at geekocephale.com
25. RETOUR À L'ANATOMIE COMPARÉERETOUR À L'ANATOMIE COMPARÉE
INTERFAÇAGE CONDITIONNELINTERFAÇAGE CONDITIONNEL
A est une instance de la type class CanSayHi si et seulement si A est également une instance de
CanSayItsName.
trait CanSayItsName[A] {
def sayMyName(a: A): String
}
implicit def greeter[A](implicit nameSayer: CanSayItsName[A]): CanSayHi[A] = new CanSayHi[A] {
override def sayHi(a: A): String = s"Hi, I'm ${nameSayer.sayMyName(a)} !"
}
26. RETOUR À L'ANATOMIE COMPARÉERETOUR À L'ANATOMIE COMPARÉE
INTERFAÇAGE CONDITIONNELINTERFAÇAGE CONDITIONNEL
Guild est une instance de la type class CanSayHi si et seulement si Player en est une instance également.
final case class Guild(members: List[Player])
implicit def guildGreeter(implicit playerGreeter: CanSayHi[Player]): CanSayHi[Guild] = new CanSayHi[Guild] {
override def sayHi(g: Guild): String = s"""Hi, we are ${g.members.map(p => playerGreeter.sayHi(p).mkString(","))}"""
}
28. CONTEXT BOUNDCONTEXT BOUND
def greet[T](t: T)(implicit greeter: CanSayHi[T]): String = ???
Peut être refactoré en (absolument identique):
def greet[T: CanSayHi](t: T): String = ???
Plus clean et exprime plus clairement la contrainte que T doit être une instance de CanSayHi
29. TYPE CLASSTYPE CLASS APPLYAPPLY
Mais comment récupère t-on notre greeter ?
... implicitly[CanSayHi[T]]...
C'est mieux !
def greet[T: CanSayHi](t: T): String = {
val greeter: CanSayHi[T] = implicitly[CanSayHi[T]]
greeter.sayHi(t)
}
object CanSayHi {
def apply[T](implicit C: CanSayHi[T]): CanSayHi[T] = C
}
def greet[T: CanSayHi](t: T): String = CanSayHi[T].sayHi(t)
30. TYPE CLASSTYPE CLASS SYNTAXSYNTAX
On peut utiliser les implicit class pour ajouter la syntax de notre type class
Ce qui nous permet d'écrire: geekocephale.greet
C'est important une bonne syntaxe !
implicit class CanSayHiSyntax[T: CanSayHi](t: T) {
def greet: String = CanSayHi[T].sayHi(t)
}
31. TOUS ENSEMBLE !TOUS ENSEMBLE !
trait CanSayHi[T] {
def sayHi(t: T): String
}
object CanSayHi {
def apply[T](implicit C: CanSayHi[T]): CanSayHi[T] = C
}
implicit class CanSayHiSyntax[T: CanSayHi](t: T) {
def greet: String = CanSayHi[T].sayHi(t)
}
final case class Player(name: String, var level: Int)
object Player {
implicit val playerGreeter: CanSayHi[Player] = new CanSayHi[Player] {
def sayHi(t: Player): String = s"Hi, I'm player ${t.name}, I'm lvl ${t.level} !"
}
}
33. SIMULACRUMSIMULACRUM
permet de se débarasser du boiler plate en le générant automatiquement, à la compilation,
grâce à des macros
Simulacrum
import simulacrum._
@typeclass trait CanSayHi[T] {
@op("greet") def sayHi(t: T): String
}
34. MAGNOLIAMAGNOLIA
permet la dérivation automatique de type classes pour les ADTs
Product types:
Sum types:
Si A et B sont des instances d'une type class T, alors C l'est aussi, "automatiquement" !
Magnolia
type A
type B
final case class C(a: A, b: B)
sealed trait C
final case class A() extends C
final case class B() extends C
35. ALLER PLUS HAUT !ALLER PLUS HAUT !
FP resources list
Anatomy of a type class
Inheritance vs Generics vs TypeClasses in Scala
Mastering Typeclass Induction
Type class, ultimate ad hoc
Type classes in Scala
Implicits, type classes, and extension methods
36. CONCLUSIONCONCLUSION
Les type classes permettent:
De ne pas mixer comportements et donnée
L'ad hoc polymorphism
D'ajouter du comportement à un type à posteriori