Functional programming
Illustrated by Scala
def pure[F[_], A](x: => A): F[A]
Qui suis-je ? (vaste question)
@ugobourdon
- CTO Performance Immo
- Programmeur indépendant
Mon boulot actuellement
Ce que j'aime faire
Agenda
- Un petit mot sur Scala
- Programmation fonctionnelle : une définition
- Type Algébrique de données
- Théorie des catégories (a.k.a Monad realm)
- Effets de bord & FP
- Programmation générique (a.k.a paramétricité)
Troll n°1
Révélation !
Scala n'est pas un langage fonctionnel
Scala est un langage purement OBJET
class ScalaIsAwesome(z: Option[String]) {
val x: String = "functional programming is great !"
var y: Int = 42
def toString = s"$x / $y / ${z.getOrElse("not here !")}"
}
case class Toto(a: String, b: Double = 0D)
object Singleton {
def g = "Le 'compilo' Scala assure que je suis un singleton"
def get = new ScalaIsAwesome(Option("yo !"))
val sayHelloFunc: String => String = x => x + " !"
}
trait MyTrait {
def f = Toto("As trait I can have implementation")
}
Scala s'appui sur ce paradigme pour proposer une API "fonctionnelle"
() => A <=> Function0[+A]
A => B <=> Function1[-A,+B]
...
Programmation fonctionelle
Ce qu'on entend à propos de FP
- adapté à des tâches mathématiques & scientifiques
- adapté à des algos complexe
- vraiment bien pour la programmation parallèle
- Mais on a besoin d'un doctorat pour comprendre
- Pas vraiment adapté aux applis de gestion
Oh que si !
Functions as first-class citizen
object Main {
// Une fonction est une valeur comme une autre
val f: Int => String = x => x.toString
// Elle peut donc être passée en paramètre d'une autre fonction
def func1(f: Int => String, y: Int): String = f(y)
// Et être retournée par une fonction
def func2(devise: String): Double => String = x => x.toString + " " + devise
// Ou les deux
def func3(f: Int => String, y: Int): String => String = f(y) + " joueurs"
}
Pures fonctions !
Conséquences
- Immutabilité
- Transparence référentielle
Prendre du recul
Le boulot du programmeur
object WhatIsMyJob {
def decompose(pb: ComplexProblem): List[LessComplexProblem]
def findSolution(pb: LessComplexProblem): Solution
def compose(actions: List[LessComplexProblem => Solution]): Application
def main(complexProblem: ComplexProblem) {
decompose(complexProblem) map { findSolution } andThen compose
}
}
Parlons un peu des types
Strong type vs weak type
Static type vs Dynamic type
The good choice !
______________________
MaClasse
val param1: String
val param2: Double
...
-------------------------------
def func1: String = ...
...
case class MyData(param1: String,
param2: Double,
...)
object DomainFunc {
def func1: String = ...
...
}
Objet vs Fonctionnel
Les données d'un côté
Les fonctions de l'autre
Comment représenter les données ?
Comment composer les fonctions entre-elles ?
Un fil conducteur
case class Contact(
firstName: String,
middleInitial: String,
lastName: String,
emailAddress: String,
emailIsVerified: Boolean
)
Qu'est-ce qui ne va pas avec cette conception ?
valeur optionnelle ?
des contraintes ?
des champs liés ?
une logique métier ?
doit être annulé si email change ?
Types algébriques de données
data ProductType = AField * AnotherField * ...
data SumType = AType | AnotherType | ...
data AlgebraicDataType = AField * AnotherField |
MyField |
Constant
...
Product & Sum Types
data UnoCard = NumericCard { value: NumericValue, color: Color } |
KickBackCard { color: Color } |
PlusTwoCard { color: Color } |
ChangeColorCard |
PlusFourCard
data NumericValue = Zero | One | Two | Three | Four | Five |
Six | Seven | Eight | Nine
data Color = Red | Blue | Green | Yellow
Exemple plus parlant
sealed trait UnoCard
case class NumericCard(value: NumericValue, color: Color) extends UnoCard
case class KickBackCard(color: Color) extends UnoCard
case class PlusTwoCard(color: Color) extends UnoCard
case object ChangeColorCard extends UnoCard
case object PlusFourCard extends UnoCard
sealed trait NumericValue
case object Zero extends NumericValue
case object One extends NumericValue
case object Two extends NumericValue
case object Three extends NumericValue
case object Four extends NumericValue
case object Five extends NumericValue
case object Six extends NumericValue
case object Seven extends NumericValue
case object Eight extends NumericValue
case object Nine extends NumericValue
sealed trait Color
case object Red extends Color
case object Blue extends Color
case object Green extends Color
case object Yellow extends Color
En Scala
Pattern matching
def color(card: UnoCard): Option[Color] = ???
def color(card: UnoCard): Option[Color] = card match {
case NumericCard(_, color) => Some(color)
case KickBackCard(color) => Some(color)
case PlusTwoCard(color) => Some(color)
case ChangeColorCard => None
case PlusFourCard => None
}
Comment faire pour atteindre le champ "color" ?
Surtout que toutes les cartes n'ont pas de couleur
Utiliser le pattern matching
case class Contact(
firstName: String,
middleInitial: String,
lastName: String,
emailAddress: String,
emailIsVerified: Boolean
)
Qu'est-ce qui ne va pas avec cette conception ?
valeur optionnelle ?
des contraintes ?
des champs liés ?
une logique métier ?
doit être annulé si email change ?
case class PersonalName(
firstName: String_50,
middleInitial: Option[String_1],
lastName: String_50)
sealed trait EmailAddress
case class VerifiedEmail(email: Email) extends EmailAddress
case class UnverifiedEmail(email: Email) extends EmailAddress
case class Contact(
name: PersonalName,
emailAddress: EmailAddress
)
object String_50 {
def apply(rawValue: String): NotValidValue \/ String_50 = ???
}
Avec les Types Algébriques de Données
Représenter l'absence de valeur
sealed trait Option[A]
case class Some[A](x: A) extends Option[A]
case object None extends Option[Nothing]
def filterValueMinus10(i: Int): Option[Int] =
if(i < 10) Some(i)
else None
Représenter une erreur
sealed trait \/[+A, +B]
case class -\/[+A](a: A) extends (A \/ Nothing)
case class \/-[+B](b: B) extends (Nothing \/ B)
def forbidValueMinus10(i: Int): IllegalArgumentException \/ Int =
if(i < 10) new IllegalArgumentException(s"value is minus than 10 [$i]").left
else i.right
Représenter un IO
def println(x: String): IO[Unit] = ??? // On verra après pour plus de détail
Théorie des catégories
C'est des maths !
La théorie des catégories étudie les structures mathématiques et les relations qu'elles entretiennent.
Catégorie ...
- un ensemble E,
- une loi de composition LC
- des règles sur E et LC
En gros
Pourquoi tu nous saoules avec des maths ?
Bon donc une fois que j'aurais passé ma thèse je pourrais commencer la programmation fonctionnelle ?
Les catégories comme "Pattern" pour résoudre des problèmes de conception
def f: Int => List[String]
def g: String => String
Comment composer f et g
val result: List[String] = f(2).map { s => g(s) }
val result: List[String] = f(2) map(g)
C'est un Foncteur !
Composer une valeur et un context avec la fonction map
trait Functor[F[_]] {
def map[A,B](f: A => B)(fa: F[A]): F[B]
}
// Tous les Functor doivent suivrent 2 lois
map (id) = id
map (f compose g) = map f compose map g
trait Exemple[A] {
def f: () => Option[A]
def g[B]: A => Option[B]
}
Comment composer f et g
val result: Option[B] = f().flatMap { a => g(a) }
val result: Option[B] = f() flatMap(g)
C'est une Monade !
Composer une valeur et une succession de contexte avec la fonction flatMap
trait Monad[M[_]] {
def point[A](a: => A): M[A]
def flatMap[A,B](ma: M[A])(f: A => M[B]): M[B]
}
// Toutes les Monades doivent suivre 3 lois
avec a: A, ma: M[A], f: A => m[B] et g : B => M[C];
flatMap(ma)(point) <=> ma // right identity
flatMap(point(a))(f) <=> f(a) // left identity
flatMap( flatMap(ma)(f))(g) <=> flatMap(ma)(x => flatMap(f(x))(g)) // associativity
Mais aussi
- Monoïde
- Applicative
- Kleisli
- ...
Pure IO
Concept
trait IO[A] {
def effect(x: => A): IO[A]
def unsafePerformIO(io: IO[A]): A
}
Utiliser le concept de lazy evaluation pour différer l'application de l'effet de bord.
Décrire l'effet de bord par un type.
Appliquer les effets à la fin du programme.
Concept
Décrire ce que le programme doit faire
Déclencher l'éxécution de ce programme à un autre moment (à la fin du programme)
println("vive")
println("la ")
println("programmation fonctionnelle !")
def println(x: String): IO[Unit]
println("vive")
.flatMap { _ => println("la ") }
.flatMap { _ => println("programmation fonctionnelle !") }
Pour utiliser le type IO, il nous faut une Monade !
Exemple
def println(x: String): IO[Unit]
object Main {
def main(id: String): Unit = {
val result = for {
_ <- println("démarre le programme")
user <- findUser(id)
_ <- println(s"l'utilisateur se nomme ${user.name}"
} yield ()
result.unsafePerformIO()
}
}
Exemple - for comprehension
Chaque ADT peut bénéficier de ces abstractions
- Option
- List
- \/ (a.k.a Either a.k.a disjonction)
- Task (async IO)
- Process (non-blocking async streaming)
- ...
Notre programme est constitué d'un ensemble de type qui décrivent des effets.
Il faut pouvoir les composer entre eux.
Les catégories (monades, foncteurs, ...) sont des abstractions utiles qui nous permettent de faire cela avec élégances.
Nous nous retrouvons alors à décrire l’exécution d'un programme, comme une suite de composition de fonction pure.
Par définition, ce programme est composable avec un autre programme de même type.
Nous pouvons donc raisonnablement (avec raison) recomposer notre décomposition qui nous permet de résoudre notre problème métier.
Paramétricité
Sommer des entiers
def sum(xs: List[Int]): Int = xs.foldLeft(0)(_+_) // notation spéciale dédicace ^^
Et pour des Double ?
def sum(xs: List[Double]): Double = xs.foldLeft(0D) { (x,y) => x + y }
Les String ?
def sum(xs: List[String]): String = xs.foldLeft("") { _ + _ }
// Même les listes !!!
def sum[A](xs: List[List[A]]): List[A] = xs.foldLeft(Nil) { _ ++ _ }
Comment supprimer cette duplication ?
def sum[A](xs: List[A]): A = xs.foldLeft(0)(_+_)
Utiliser un type paramétré (générique)
mais A n'a pas de méthode (+) ni de zéro
Classes de type
a.k.a Typeclass
Typeclass
def sum[A](xs: List[A])(implicit m: Monoid[A]): A = xs.foldLeft(m.zero)(m.plus)
trait Monoid[M] {
def zero: M
def plus(x: M, y: M): M
}
Et sinon concrètement ?
def sum[A](xs: List[A])(implicit m: Monoid[A]): A = xs.foldLeft(m.zero)(m.plus)
trait Monoid[M] {
def zero: M
def plus(x: M, y: M): M
}
import Monoid.IntMonoid
val ints = 1 :: 2 :: 3 :: 4 :: Nil
sum(ints) shouldBe 10
object Monoid {
implicit object IntMonoid extends Monoid[Int] {
def zero: Int = 0
def plus(x: Int, y: Int) = x + y
}
}
Double, String, List, Price, ...
Free Theorem
// Cette fonction multiplie par deux
def f(x: Int): Int = ???
Que fait f ?
// Cette fonction multiplie par deux
def f(x: Int): Int = x + 9
Les commentaires ce n'est pas fiable
def addNine(x: Int): Int = ???
mieux vaut nommer ses fonctions
def addNine(x: Int): Int = x * 2
Ah bas en fait non :)
Et là que fait f ?
def f[A](x: A): A = ???
Une seule implémentation possible
def f[A](x: A): A = x
Les types ne mentent pas
On en déduit
- Les commentaires mentent
- Les noms de fonctions mentent
- Seul les Types ne mentent pas
Les types de mentent pas
def f[A](x: A): A = x
Mais il faut généraliser pour pouvoir restreindre les choix d'implémentation
Un autre exemple
def compose[A,B,C](g: B => C, f: A => B): A => C = (x: A) => g(f(x))
def compose[A,B,C](g: B => C, f: A => B): A => C = g compose f
def compose[A,B,C](g: B => C, f: A => B): A => C = f andThen g
Un contre-exemple ?
def reverse[A](xs: List[A]): List[A]
Que peut on dire ?
Tous les éléments retournés par reverse se trouvent dans la liste d'entrée
Property based testing
def reverse[A](xs: List[A]): List[A]
∀x. reverse(reverse(x)) == x
∀x. ∀y. reverse(x ++ y) == reverse(y) ++ reverse(x)
cf scalacheck, quickcheck, etc...
Paramétricité
- Les types sont la seule documentation fiable
- S'ils ne suffisent pas, les tests de propriétés sont là
- Paramétricité permet d'enlever bcp de duplication (grâce aux classes de type)
- OCP (Open Close Principle) est implémenté avec facilité & élégance => code robuste
Conclusion
- Facile à tester
- Programmation "sure"
- Abstractions puissantes
- Un programme explicite | descriptif
- Documentation métier riche
- Élimination de la duplication facilitée
Conclusion
- Choisir un langage "pur"
- ou s'astreindre à une discipline rigoureuse
- Montée en complexité initiale par rapport à la programmation impérative
- mais plus simple ensuite (= moins compliqué)
- Paradigme qui progresse dans la communauté des dev mais pas encore très répandu
FP everywhere ?
Appliquer les concepts dans d'autres contextes
Du code fonctionnel partout
- Back (Scala, F#, Haskell, ...)
- Front (javascript?, purescript, elm, ghc.js, ...)
Immutable/functional Database
Immutable/functional Infrastructure
FP in Scala
By ugobourdon
FP in Scala
Introduction to functional programming with example in Scala
- 2,084