Functional programming

Illustrated by Scala

def pure[F[_], A](x: => A): F[A]

Qui suis-je ? (vaste question)

@ugobourdon

Mon boulot actuellement

Ce que j'aime faire

Agenda

  1. Un petit mot sur Scala
  2. Programmation fonctionnelle : une définition
  3. Type Algébrique de données
  4. Théorie des catégories (a.k.a Monad realm)
  5. Effets de bord & FP
  6. 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,102