Scala : une EXCELLENTE introduction à la programmation fonctionnelle.


Je vais affirmer une évidence, mais la programmation fonctionnelle, c’est ultra-cool !

Elle permet de réduire l’architecture d’un logiciel à des concepts simples, mais très puissants, tout en rendant le code plus compact, plus sécurisé, plus facile à maintenir et à tester, bref, elle mérite son pesant de cacahuètes.

Mais alors, pourquoi est-ce qu’aussi peu de développeurs l’utilisent comparé à la programmation orientée objets (POO) ?


Le problème de la PF

Déjà, si vous êtes néophyte de la programmation fonctionnelle (PF), je vous invite d’abord à aller lire cet article afin de comprendre comment elle fonctionne et pourquoi elle est aussi efficace.

En plus, notre industrie évolue de plus en plus vers ce genre de design, donc vous n’avez aucune raison de vous en priver.

Ce qu’il faut savoir, c’est qu’on peut faire de la PF avec quasiment tous les langages : C#, Java, JavaScript, Python…

Mais bien sûr, ce n’est pas aussi efficace que de maîtriser un véritable langage de PF, comme Haskell, Clojure ou F#, car ils implémentent ses concepts mieux que n’importe quel autre langage.

Sauf que pour quelqu’un qui n’a pas l’habitude, ces langages sont assez brutaux à aborder. Je vous laisse juger par ce code en Haskell tout simple, qui permet de réaliser un chiffrement par décalage.

module Caesar (caesar, uncaesar) where 
 
import Data.Char
 
caesar, uncaesar :: (Integral a) => a -> String -> String
caesar k = map f
    where f c = case generalCategory c of
              LowercaseLetter -> addChar 'a' k c
              UppercaseLetter -> addChar 'A' k c
              _               -> c
uncaesar k = caesar (-k)
 
addChar :: (Integral a) => Char -> a -> Char -> Char
addChar b o c = chr $ fromIntegral (b' + (c' - b' + o) `mod` 26)
    where b' = fromIntegral $ ord b
          c' = fromIntegral $ ord c
Quand on connaît pas, ça fait un choc.

Et c’est normal, car la plupart d’entre nous sommes habitués à la POO.

Passer à la PF demande d’approcher les problèmes avec un nouvel angle, concevoir des applications d’une nouvelle manière, et d’apprendre des nouvelles technologies et patrons de conception.

Malheureusement, peu de développeurs sont capables (ou font l’effort) de faire cette gymnastique mentale, tout comme certains “vieux de la vieille” n’ont jamais réussi la transition entre PP et POO.

Et c’est dommage parce que ce frein à l’apprentissage de la PF se fait ressentir dans les deux camps :

  • Les développeurs POO ne profitent pas des avantages qu’offre la PF en terme d’architecture et de bonnes pratiques.
  • La PF ne se démocratise pas aussi rapidement que certains le voudraient.

Comme le dit très bien Richard Feldman dans sa conférence “Pourquoi la programmation fonctionnelle n’est pas la norme ?”, celle-ci manque de killer app.

La plupart des développeurs n’ont pas d’intérêt IMMÉDIAT à l’utiliser, donc ils ne l’apprennent pas, même si elle peut les aider sans qu’ils le sachent.

La meilleure solution serait de trouver un entre-deux, un langage qui permettrait aux développeurs POO de profiter des avantages de la PF sans perdre leurs bonnes habitudes, et par la même occasion, populariser cette méthode auprès du grand public.

Et aujourd’hui, j’aimerais parler du langage qui, pour moi, représente le juste-milieu parfait entre POO et PF : Scala.


Pourquoi Scala ?

Scala est un langage de programmation à plusieurs paradigmes sorti en 2004. Il possède plusieurs implémentations, dont la plus connue est basée sur la JVM, à l’instar de Java et Kotlin.

Contrairement à ses collègues OO (en particulier Java et C#), Scala profite d’une syntaxe ultra-épurée qui permet de faire énormément de choses en très peu de lignes de code. Un courant d’air frais pour les développeurs qui n’aiment pas trop écrire. Si c’est votre cas, je vous recommande ça aussi.

public List<Product> getProductsByCategory(String category) {
    List<Product> products = new ArrayList<Product>();
    for (Order order : orders) {
        for (Product product : order.getProducts()) {
            if (category.equals(product.getCategory())) {
                products.add(product);
            }
        }
    }
    return products;
}
Un bout de code en Java.
def productsByCategory(category: String) = orders.flatMap(o => o.products).filter(p => p.category == category)
Le même bout de code en Scala.

Mais le gros avantage de Scala, c’est qu’il combine POO et PF de manière parfaitement harmonieuse, sans passer par des incantations vaudoues sorties tout droit d’un roman de Lovecraft. Il cumule ainsi les avantages :

  • De la POO : encapsulation, abstraction…
  • De la PF : immutabilité, fonctions pures…

Tout ça sans que votre code devienne un véritable monstre en spaghetti volant.

Vous savez, celui qui a créé l’univers un jour où il était bourré.

Je vous sens sceptique, mais je vous assure que c’est bien réel, et je vous propose de voir tout de suite comment ça se concrétise.

Mais attention, cet article n’a pas pour but de traiter le cas Scala dans son intégralité, mais simplement de vous donner les billes qui vous permettront de comprendre son fonctionnement, mais surtout son intérêt.

Si l’envie vous vient de l’apprendre, ce que je vous recommande fortement si vous débutez en programmation fonctionnelle, je vous propose ma méthode ultra-efficace pour apprendre les bases d’un langage de programmation en moins d’une semaine.

Sur ce, commençons.


Concepts

Données

Scala permet de manipuler deux sortes de données :

Les valeurs : issues de la PF. Il s’agit de données immutables, c’est-à-dire qu’on ne peut pas les modifier après leur initialisation. Ce ne sont que des constantes.

val x = 0

// Ne compile pas
// x += 1

Les variables : les données mutables que l’on connaît tous.

var y = 0

// Compile !
y += 1

Dans le cadre d’une architecture fonctionnelle, on préférera quasiment toujours utiliser des valeurs.

Les variables sont surtout là pour répondre aux limitations des valeurs (comme un pointeur dans une boucle for qui s’incrémente à chaque fois), mais ne doivent en aucun cas changer l’état du programme ou causer des effets secondaires.

def forLoop(range : Int) = {
    var i = 1
        for(i <- 1 to range) {
        println(i)
    }
}

Bien sûr, en dehors d’une architecture fonctionnelle, l’utilisation de variables est parfaitement acceptée. Mais il faut garder en tête que l’immutabilité reste une bonne pratique même en POO. N’est-ce pas François, toi et tes variables globales à la ***.


Comportement

Comme pour les données, on retrouvera deux sortes de comportement en Scala :

Les fonctions : issue de la PF. On parle ici de fonctions pures qui ne font que prendre des paramètres en entrée et sortir un résultat. Il n’y a pas de syntaxe particulière, il ne s’agit ni plus ni moins que d’une valeur à laquelle on attache un comportement.

val double = (x: Int) => x * 2
println(double(4)) // 8

Les méthodes : issue de la POO. Il s’agit de fonctions, généralement (mais pas toujours) plus complexes, pouvant générer des effets secondaires.

def triple(x: Int) : Int = x * 3
println(triple(4)) // 12

Il est facile de faire un amalgame entre les méthodes et les fonctions tant elles sont similaires. Mais voici les différences clef à prendre en compte.

  • Les fonctions ne sont évaluées qu’une seule fois, tandis que les méthodes sont évaluées à chaque appel. Ainsi, les fonctions sont en général plus performantes que les méthodes.
  • Les fonctions ne sont que des valeurs : elles peuvent être composées et passées en paramètre d’autres fonctions. C’est un des principes fondamentaux de la programmation fonctionnelle.
def sum(callback : (Int) => Int, range : Int) = {
    var i : Int = 0
    var res : Int = 0

    for(i <- 1 to range) {
    	res += callback(i)
    }

    res
}

println(sum(double, 10)) // 110
  • Les méthodes possèdent beaucoup plus de fonctionnalités : surcharge, paramètres optionnels…
  • Les méthodes permettent les changements d’état et les effets secondaires.

À noter que les méthodes ne sont pas exclusives à la POO et peuvent être utilisées dans un environnement fonctionnel, même si, comme pour les variables, c’est plus rare et pas toujours recommandé.

Donc encore une fois, ne soyez pas discriminant de l’un ou l’autre, et utilisez celle qui correspond le mieux à la situation.


Structures

Les structures permettent d’encapsuler et de compartimenter les données et les comportements afin que le code ne devienne pas un fourre-tout informe.

Scala propose 4 types de structures :

Les classes : issue de la POO. Elles permettent d’encapsuler des données (valeurs et variables) ainsi que des comportements (fonctions et méthodes). Elles permettent de créer des instances mutables.

class Sandwich(pBrandButter : String, pTypeSalad : String) {
    // Values
    val brandButter = pBrandButter

    // Variables
    var typeSalad = pTypeSalad

    // Fonctions
    val tomatoSlices = (tomatoes : Int) => tomatoes * 6

    // Méthodes
    def switchSalad(newTypeSalad : String) = {
    typeSalad = newTypeSalad
    }
}

// Même en étant déclaré comme valeur, les propriétés variables de l'objet restent mutables
val sandwich = new Sandwich("Fruit d'or", "Roquette")

println(sandwich.brandButter) // "Fruit d'or"
println(sandwich.tomatoSlices(4)) // 24

println(sandwich.typeSalad) // "Roquette"
sandwich.switchSalad("Mache")
println(sandwich.typeSalad) // "Mache"
Vous remarquerez que Scala a une manière particulière de construire ses objets. La définition du constructeur se fait dans la définition de la classe, et l’implémentation du constructeur se fait directement dans le corps de la classe.

Les case classes : issue de la PF. Il s’agit de classes qui ne contiennent que des valeurs. Elles permettent de créer des instances immutables, et il est possible de les comparer entre eux, ce qui n’est pas le cas des classes normales.

case class Burger(sauce : String, saladType : String, tomatoSlices : Int, steakType : String)

val burger = new Burger("Ketchup", "Scarole", 3, "Boeuf")
println(burger.sauce) // "Ketchup"
println(burger.tomatoSlices) // 3
En Java et C#, cette structure de données est appelée record.

Les traits : il s’agit de structures pouvant contenir des données et des comportements, mais qui ne sont pas instanciables. Elles ont pour seul et unique but d’être hérités par des classes.

Les données/comportements peuvent avoir une implémentation, auquel cas le trait joue le rôle d’une classe abstraite. Mais pas toujours, auquel cas le trait joue le rôle d’une interface. Il est aussi possible de faire les deux en même temps dans un seul trait.

trait Hamburger {
    // Deux variable définie
    val breadType : String = "Pain à burger" 
    val steakType : String = "Boeuf"

    // Une valeur nécéssitant d'être définies
    val cheeseType : String

    // Une méthode définie
    def eat(): Unit = println("Eating...")

    // Une méthode nécéssitant d'être définie
    def waste(): Unit
}

class BigGreen(pCheeseType : String, pSaladType : String, pTomatoSlices : Int, pPicklesSlices : Int, pOnionSlices : Int) extends Hamburger {
    override val steakType : String = "None"
    val saladType : String = pSaladType
    val tomatoSlices : Int = pTomatoSlices
    val picklesSlices : Int = pPicklesSlices
    val onionSlices : Int = pOnionSlices
    val cheeseType : String = pCheeseType

    def waste(): Unit = println("Wasted :(")
}

val bigGreen = new BigGreen("Emmental", "Roquette", 2, 3, 4)
println(bigGreen.breadType) // "Pain à burger"
println(bigGreen.steakType) // "None"
println(bigGreen.saladType) // "Roquette"
println(bigGreen.cheeseType) // "Emmental"
bigGreen.eat() // "Eating..."
bigGreen.waste() // "Wasted :("

Les objets : À ne pas confondre avec les instances de classes. Il s’agit de singletons principalement utilisés pour grouper les données et comportements statiques. Ils ne sont pas instanciables et s’appellent directement par leur nom comme les classes statiques en Java.

class Meal
class Ingredient

object Cuisine {
	// Syntaxe pour les méthodes pas encore implémentées (oui, j'avais la flemme)
    def mix(meal : Meal, duration : Int) : Meal = ???
    def heat(duration : Meal, temperature : Float) : Meal = ???
    def add(meal : Meal, ingredient : Ingredient) : Meal = ???
}

val meal = Cuisine.mix(new Meal, 60)

D’ailleurs, contrairement à Java où le point d’entrée se trouve dans une méthode, en Scala, il se trouve directement dans le corps (le constructeur) de l’objet Main.

public class Main {
    public static void main(String[] args) {
        System.out.println("Hello World!");
    }
}
Hello World! en Java.
object Main extends App {
	println("Hello World!")
}
Hello World! en Scala.

Types de données

Étant donné que Scala est un langage basé sur la JVM, les types sont mappés sur ceux de Java et sont répartis sur plusieurs niveaux :

  1. Le type Any : au sommet de la pyramide. Tous les autres types héritent de lui, ce qui fait qu’il s’associe avec tout le monde. Il est le plus haut niveau d’abstraction possible, comme le type Object en Java.
  2. Les types AnyVal et AnyRef : ils héritent directement de Any et ils ne servent qu’à séparer les types valeurs des types références.
  3. Tous les autres types (int, float, string, les classes, les case classes…) : ils héritent soit de AnyVal, soit de AnyRef. Par défaut, toutes les classes héritent de AnyRef.
// Compile
val bixMax : AnyRef = new Burger("Ketchup", "Scarole", 3, "Boeuf")
// Ne compile pas
// val bixMax : AnyVal = new Burger("Ketchup", "Scarole", 3, "Boeuf")
  1. Le type Nothing : en bas de la pyramide, il hérite de tous les types, ce qui fait qu’il ne s’associe avec personne. Il est surtout utilisé pour les expressions qui ne retournent rien, comme le type void en Java.

Et comme une image vaut mieux qu’un long discours :

Scala possède bien une valeur null, mais elle existe uniquement pour la compatibilité avec la JVM. En alternative, il faut utiliser le trio Option/Some/None.

Voilà pour les bases de Scala. Tout le reste, c’est du classique : conditions, boucles… Que je ne ferais pas l’affront de vous présenter.

En revanche, si vous voulez creuser plus en profondeur et découvrir toutes ses subtilités hyper-intéressantes, je vous renvoie vers ma méthode pour apprendre un langage de programmation très rapidement, que j’ai bien sûr utilisée pour écrire cet article.

Ok, finis la théorie, place à la pratique.


Démonstration

Pour cette démonstration, je vais reprendre l’exemple que j’avais fait dans mon article sur la programmation fonctionnelle, en le pimpant un petit peu.

Bien que totalement inutile, il permet de coupler PF et POO et montrer la complémentarité entre les deux.

Le principe est simple : un “jeu” dont le but est de doser les ingrédients d’un café. On veut que l’utilisateur rentre certains paramètres, et à la fin, le programme va impitoyablement juger si son café est de qualité ou non.

Oui, c’est très con, et comme j’ai bien la flemme, il s’agira d’une application console.

Elle sera divisée en deux parties :

  • L’environnement fonctionnel, qui profitera (et abusera) des concepts de la PF (valeurs, fonctions, case classes…).
  • Les règles métier, qui feront intervenir des concepts de la POO (variables, méthodes, classes) et qui vont utiliser l’environnement fonctionnel.

La première étape consiste à créer les types, à savoir les ingrédients et leurs intermédiaires. Comme il s’agit d’un petit programme, je n’en ai fait que deux, Coffee et Water. Mais on pourrait en imaginer d’autres : HotWater, InstantCoffee, Sugar…

Et comme ces éléments n’ont pas besoin de comportement, ni de changer d’état, il s’agira de… case classes, vous avez saisi !

/**
 * @param quantity En millilitres
 * @param temperature En degrès Celsius
 */
case class Water(
    quantity : Double, 
    temperature : Double
)

/**
 * @param coffee En grammes
 * @param water
 * @param sugar En grammes
 */
case class Coffee(
    coffee : Double, 
    water : Water, 
    sugar : Double
)
Les cases classes peuvent bien sûr être composées d’autres cases classes.

Passons maintenant aux fonctions. Déjà, il y a celles qui permettent de doser et d’assembler des ingrédients afin de créer le produit final.

Pour corser la difficulté, les doses doivent être faites avec les moyens du bord : tasses, cuillères à soupe et carrés de sucre, sinon, c’est trop facile 😏

/** Conversion cuillère à soupe / grammes
 * @param tablespoons Nombre de cuillères à soupe (divisible)
 */
val addTablespoonOfCoffee = (tablespoons : Double) => tablespoons * 15

/** Conversion tasse / mL
 * @param cups Nombre de tasses (divisible) = 8oz
 */
val addCupOfWater = (cups : Double) => cups * 237

/** Conversion cube de sucre / grammes
 * @param cubes Nombre de cubes (divisible)
 */
val addSugarCubes = (cubes : Double) => cubes * 2.3

/** Chauffage de l'eau au micro-ondes
 * @param quantity Quantité d'eau (mL)
 * @param energy Puissance du micro-ondes (W)
 * @param duration Durée du chauffage (s)
 */
val boilWaterWithMicrowave = (quantity : Double, energy : Int, duration : Int) => new Water(quantity, (energy * duration)/(4.2 * quantity) + 20)

/** Création du café
 * 
 * Synthèse de toutes les fonctions précédentes.
 */
val makeCoffeeAtHand = (
    coffeeQuantity : Double, 
    waterQuantity : Double,
    microwaveEnergy : Int, 
    microwaveDuration : Int,
    sugarQuantity : Double) => {
    new Coffee(
        addTablespoonOfCoffee(coffeeQuantity), 
        boilWaterWithMicrowave(
            addCupOfWater(waterQuantity),
            microwaveEnergy,
            microwaveDuration),
        addSugarCubes(sugarQuantity))
}
Oui, je fais chauffer de l’eau au four à micro-ondes, y a un problème ?

Ensuite, j’ajoute la méthode qui permettra de faire un bilan du dosage. Bien sûr, étant donné que l’on est dans une architecture fonctionnelle, on retourne un message au lieu de l’afficher directement, car sinon, il s’agirait d’un effet secondaire, l’antichrist de la PF.

def evaluateCoffeeQuality(coffee : Coffee) : String = {
    // Ratio café:eau idéal : 1:7.9 - 1:15.8
    if(coffee.coffee * 15.8 < coffee.water.quantity) {
        return "Le cafe est trop leger, ajoute en un peu plus !"
    }
    else if(coffee.coffee * 7.9 > coffee.water.quantity) {
        return "Le cafe est trop fort, attention a la cafeine !"
    }

    // Température idéale : 35-70
    if(coffee.water.temperature > 70) {
        return "Fait gaffe, le cafe est giga chaud."
    }
    else if(coffee.water.temperature < 35) {
        return "C'est mort, le cafe est froid..."
    }

    // Ratio sucre:café maximum : 1:3
    if(coffee.sugar > coffee.coffee / 3) {
        return "T'as abuse sur le sucre, attention au diabete"
    }

    return "Tout est OK, bonne degustation ;)"
}
Prenez mes dosages recommandés avec une pincée de sel, je suis loin d’être un expert 😇

Et enfin, je me permets d’ajouter quelques méthodes de conversion de données, de manière à pouvoir intervenir si jamais l’utilisateur rentre une chaîne de caractères au lieu d’un nombre.

object Parse {

    def toInt(str : String): Option[Int] = {
        try {
            Some(str.toInt)
        }
        catch {
            case e: NumberFormatException => None
        }
    }

    def toDouble(str : String): Option[Double] = {
        try {
            Some(str.toDouble)
        }
        catch {
            case e: NumberFormatException => None
        }
    }
}
En PF, on ne lance JAMAIS d’exception, toutes les fonctions doivent retourner quelque chose. C’est au moment des règles métier que l’on choisi si ce quelque chose doit faire planter le programme ou non.

Voilà pour l’environnement fonctionnel, passons maintenant aux règles métier. Tout ce qu’on va faire, c’est venir récupérer les entrées de l’utilisateur et imprimer le bilan dans la console. Ça devrait être facile.

object Main extends App {
    def parseInputToInt(): Int = {
        Parse.toInt(readLine()) match {
            case Some(i) => i
            case None => throw new Exception("Le texte d'entre doit etre un nombre.")
        }
    }

    def parseInputToDouble(): Double = {
        Parse.toDouble(readLine()) match {
            case Some(i) => i
            case None => throw new Exception("Le texte d'entre doit etre un nombre.")
        }
    }

    println("Tasse(s) d'eau")
    val waterQuantity = parseInputToDouble()
    
    println("Energie du micro-ondes")
    val microwaveEnergy = parseInputToInt()

    println("Duree du micro-ondes")
    val microwaveDuration = parseInputToInt()

    println("Cuillere(s) de cafe")
    val coffeeQuantity = parseInputToDouble()

    println("Cube(s) de cafe")
    val sugarQuantity = parseInputToDouble()

    val coffee : CoffeeMaker.Coffee = CoffeeMaker.makeCoffeeAtHand(coffeeQuantity, waterQuantity, microwaveEnergy, microwaveDuration, sugarQuantity)

    println(CoffeeMaker.evaluateCoffeeQuality(coffee))
}
Dans ce cas, le None fait planter le programme. En alternative, on pourrait maintenir l’utilisant dans une boucle tant que parseInput retourne None.

Quelques petits tests pour voir ce que ça donne.

Et voilà, un petit programme à la con, mais qui permet de voir comment Scala profite de la POO et de la PF en gardant le tout propre et cohérent !

Pour compléter cet article, je vous mets le lien vers la conférence de Jordan Parmer qui fait un parser permettant de valider des formulaires avec Scala. Un exemple beaucoup plus complexe, mais avec un réel intérêt dans le monde réel.


Au final, je vous invite vraiment à vous intéresser à Scala. C’est compact, puissant, et une très bonne introduction à la PF sans être dépaysé. Bref, c’est certainement un de mes langages préférés des années 2000.

Et bien sûr, il ne s’agit pas d’un projet étudiant, mais bien d’un langage réputé qui a été utilisé pour de nombreux projets, tels que Kafka, Spark, Akka.io, Play Framework… Ainsi que par beaucoup d’entreprises, comme Twitter, Apple, Verizon, The Guardian, Tumblr…

Aussi, si vous voulez apprendre Scala très rapidement, je vous renvoie vers ma méthode d’apprentissage des langages de programmation.

Et si vous voulez voir une vraie application métier faite avec Scala, laissez-moi un commentaire juste en dessous et je retrousserai mes manches pour faire un second article bien plus technique.

Quant à moi, je vous laisse, il faut que j’aille livrer 12 tonnes de canne à sucre au Congo. Je vous donne rendez-vous la semaine prochaine pour un nouvel article sur le blog des développeurs ultra-efficaces !


Accéder au bonus : Apprendre un langage de programmation de A à Z.