def pure[F[_], A](x: => A): F[A]
Qui suis-je ? (vaste question)
@ugobourdon
Mon boulot actuellement
Ce que j'aime faire
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]
...
Oh que si !
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"
}
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
}
}
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 ?
case class Contact(
firstName: String,
middleInitial: String,
lastName: String,
emailAddress: String,
emailIsVerified: Boolean
)
valeur optionnelle ?
des contraintes ?
des champs liés ?
une logique métier ?
doit être annulé si email change ?
data ProductType = AField * AnotherField * ...
data SumType = AType | AnotherType | ...
data AlgebraicDataType = AField * AnotherField |
MyField |
Constant
...
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
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
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
)
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 = ???
}
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
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
def println(x: String): IO[Unit] = ??? // On verra après pour plus de détail
La théorie des catégories étudie les structures mathématiques et les relations qu'elles entretiennent.
En gros
Bon donc une fois que j'aurais passé ma thèse je pourrais commencer la programmation fonctionnelle ?
def f: Int => List[String]
def g: String => String
val result: List[String] = f(2).map { s => g(s) }
val result: List[String] = f(2) map(g)
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]
}
val result: Option[B] = f().flatMap { a => g(a) }
val result: Option[B] = f() flatMap(g)
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
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.
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 !
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()
}
}
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.
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) { _ ++ _ }
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
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
}
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, ...
// Cette fonction multiplie par deux
def f(x: Int): Int = ???
// Cette fonction multiplie par deux
def f(x: Int): Int = x + 9
def addNine(x: Int): Int = ???
def addNine(x: Int): Int = x * 2
def f[A](x: A): A = ???
def f[A](x: A): A = x
def f[A](x: A): A = x
Mais il faut généraliser pour pouvoir restreindre les choix d'implémentation
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
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
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...