← Retourner à la liste des articles
Image blog
Auteur

Par Maxime Jumelle

CTO & Co-Founder

Publié le 28 févr. 2022

Catégorie Data Engineering

Scala : tout savoir sur le langage fonctionnel

Scala tire son nom de « scalable language » car il a été pensé pour être utilisé à n'importe quelle échelle : de simples scripts jusqu'à de larges systèmes. Développé initialement en 2004 à l'EPFL, Scala s'intègre complètement avec Java et s'exécute en tant que plateforme Java.

Aujourd'hui, le langage Scala est principalement utilisé dans les applications Big Data, car elles sont historiquement développées en Java, mais ce dernier est quelques fois trop lourd au niveau de la syntaxe et manque surtout d'un paradigme très utile pour des opérations de calculs distribués : la programmation fonctionnelle.

Logo Scala

Les deux paradigmes du langage Scala

On considère souvent que Scala est un langage difficile. En principe, tous les langages sont difficiles : en maîtriser un signifie bien plus que connaître par cœur la syntaxe. Dans les faits, Scala peut sembler déroutant aux premiers abords car il mélange deux paradigmes.

  • La programmation orienté objet, devenue un standard dans le développement et très présent en Java, C# ou C++, où l'on construit des objets et des relations entre ces derniers.
  • La programmation fonctionnelle, qui puise ses origines du \(\lambda\)-calcul, où tout est considéré comme étant des fonctions mathématiques sur des structures algébriques (popularisé à ses débuts par le langage Haskell).

Et c'est en partie à cause de ce double paradigme que les nouveaux utilisateurs de Scala peuvent avoir des difficultés.

Les développeurs Java qui, par exemple, sont très attachés à l'orienté objet et à l'impératif (boucle for), ont beaucoup de difficultés à utiliser l'approche fonctionnelle, car ce n'est pas dans leurs habitudes. De plus, il y a beaucoup de sucre syntaxique dans Scala, c'est-à-dire de formatage de code plus digeste et plus abrégé, en comparaison avec Java qui est beaucoup plus verbeux.


À lire aussi : découvrez notre formation Data Engineer


Les développeurs Python ou R (Data Scientist par exemple) ont déjà en tête l'utilisation de certains paradigmes fonctionnels (apply sous pandas ou avec dplyr), et la syntaxe de Scala leur est beaucoup plus familière. En revanche, ces deux langages n'intègrent que peu d'orienté objet et le typage est plus dynamique, ce qui perturbe également ces développeurs.

Paradigme fonctionnel

La programmation fonctionnelle est un des piliers du langage Scala. C'est sa principale différence avec le Java et bien qu'il permette de faire du fonctionnel, cela nécessite beaucoup plus de code avec une syntaxe pas toujours adaptée. De plus en plus de frameworks tendent vers cette approche fonctionnelle, car elle possède de nombreux avantages.

  • Elle permet de définir un cadre mathématique rigoureux.
  • Elle est adaptée dans les situations de calculs distribués.
  • Elle offre de nombreuses possibilités pour le sucre syntaxique.

Certains frameworks comme Spark, Akka ou encore Flink utilisent beaucoup cette approche fonctionnelle (tout en n'oubliant pas le paradigme orienté objet), où les fonctions en forment la composante principale.

Paradigme objet

L'orienté objet est un paradigme de programmation où l'on définit et instancie des objets. Ces objets peuvent être de différentes natures, comme les classes, les interfaces ou les modules. Le langage Scala requiert l'utilisation de l'orienté objet en coordination avec le paradigme fonctionnel : cela nécessite l'utilisation de certaines notions déjà présentes en Java comme les types génériques, mais on retrouve également de nouveaux concepts comme les cases classes, les objets compagnons ou les contraintes de types.

Structures et types

Sous Scala, il existe de nombreuses implémentations de structures de collections. Elles sont bien évidemment indispensables dans la plupart des cas, et l'on retrouve les structures classiques.

  • Les séquences, où les éléments sont ordonnés par leur position : on retrouve par exemple les List, les Stack ou encore les Vector.
  • Les ensembles, dont les ListSet ou les HashSet.
  • Les dictionnaires, où les éléments sont représentés par des couples clés/valeurs, tels que les ListMap ou TreeMap.

Ces collections sont par défaut immuables : cela signifie que l'on ne peut pas modifier leurs instances. Par exemple, dans une liste immuable, il n'est pas possible d'ajouter ou de modifier les éléments. Il faudrait dans ce cas créer une nouvelle variable ou utiliser une liste mutable.

// Manipulation de listes en Scala
val maListe = List(1, 2, 3, 4)
println(maListe.head) // 1
println(maListe.tail) // List(2, 3, 4)
println(0 +: maListe) // List(0, 1, 2, 3, 4)
println(0 :: maListe) // List(0, 1, 2, 3, 4) = Équivalent à l'opérateur précédent
println(maListe :+ 5) // List(1, 2, 3, 4, 5)
println(List(-1, 0) ::: maListe) // List(-1, 0, 1, 2, 3, 4)
println(maListe ::: List(5, 6)) // List(1, 2, 3, 4, 5)

Le choix de la bonne structure est essentiel pour chaque situation. En effet, sous Scala, on s'intéresse beaucoup à la notion de complexité de calcul, et on cherche toujours à proposer des algorithmes les plus rapides.

L'exemple le plus primordial concerne les listes. La représentation d'une liste en Scala est celle d'une liste chaînée : ajouter un élément en début de liste est quasi-immédiat (complexité \(O(1)\)), alors que rajouter un élément en fin de liste suppose de parcourir toute cette liste (complexité \(O(n)\) avec \(n\) la taille de la liste), ce qui est beaucoup moins optimisé.

// Concaténation à droite
val grandeListe = (1 to 100000).toList
var timerDroite = System.nanoTime()
grandeListe :+ 0
var dureeDroite = (System.nanoTime() - timerDroite) / 10e9
println(f"Durée de la concaténation à droite : $dureeDroite")
Durée de la concaténation à droite : 6.679373E-4

Et maintenant, pour une concaténation à gauche.

// Concaténation à gauche
var timerGauche = System.nanoTime()
0 +: grandeListe
var dureeGauche = (System.nanoTime() - timerGauche) / 10e9
println(f"Durée de la concaténation à gauche : $dureeGauche")
println(f"La concaténation à droite est ${dureeDroite / dureeGauche}%.2fx plus lente.")
Durée de la concaténation à gauche : 7.079E-7
La concaténation à droite est 943,55x plus lente.

Opérations HOF

Les Higher-Order Functions (HOF) sont très importantes en programmation fonctionnelle. En effet, ce sont elles qui permettent d'implémenter du \(\lambda\)-calcul programmable. Une HOF est une fonction \(F\) qui prend en arguments au moins une fonction et retourne une fonction comme résultat.

Parmi les HOF les plus populaires, on retrouve notamment la fonction map. La fonction map sur une collection \([x_1, \dots, x_n]\) applique une fonction \(f\) sur chaque élément de la collection et retourne le même type de collection \([f(x_1), \dots, f(x_n)]\).

$$\text{map}(f)([x_1, \dots, x_n])=[f(x_1), \dots, f(x_n)]$$

Par exemple, supposons avoir la liste suivante.

val maListFor = List(1, 2, 3, 4)

Et que l'on souhaite appliquer un traitement à chaque élément de cette liste, et retourner ces résultats dans une liste. Plutôt que d'utiliser une boucle for (déconseillée en Scala), on peut utiliser la fonction map.

println { maListFor.map(_ * 2) }
println {
  maListFor.map(x => {
    if (x <= 2) x
    else x * 2
  })
}
println { maListFor.map(x => List(x, x * 2)) }
List(2, 4, 6, 8)
List(1, 2, 6, 8)
List(List(1, 2), List(2, 4), List(3, 6), List(4, 8))

Il existe plusieurs opérations HOF (flatMap, reduce, fold, ...), et elles sont fondamentales dans l'approche fonctionnelle.

Objets et classes

Les classes constituent la base de l'orienté objet. Une classe est un objet qui contient plusieurs champs/propriétés (variables, méthodes, constructeur) et qui peut être instancié en tant que variable.

Contrairement au Python, on utilise beaucoup d'objets et de classes dans les projets et les scripts Scala. Il est donc courant de créer des classes de petites tailles pour des besoins précis ou d'hériter depuis des classes de frameworks existant pour adapter son code à son projet.

Une première classe

Modélisons une première classe qui représente un ordinateur que nous allons nommer Computer.

  • Chaque ordinateur doit disposer des variables brand (immuable) qui indique la marque de l'ordinateur et os (mutable) qui indique le système d'exploitation.
  • Seules deux méthodes seront présentes : start et stop, qui vont toutes les deux agir sur une variable isOn indiquant l'état de l'ordinateur (allumé/éteint).
class Computer(val brand: String, var os: String) {
  var isOn = false // L'ordinateur est allumé ?
  def start(): Unit = this.isOn = true
  def stop(): Unit = this.isOn = false
}

Ce qui est particulier ici, c'est que le constructeur (méthode d'initialisation) se définit directement après le nom de la classe, là où en Java, il faudrait créer un constructeur spécifiquement.

// Code Java
class Computer {

  Computer(String os, String brand) {
    // ...
  }

  // ...
}

Héritage

L'héritage est un concept qui permet de dériver une classe en une autre plus spécifique tout en conservant les propriétés déjà présentes.

Pour cela, on utilise le mot réservé extends.

class Laptop(brand: String, os: String) extends Computer(brand, os) {
  var isOpen = false
  def open(): Unit = isOpen = true
  def close(): Unit = {
    if (isOn) super.stop()  // Fermer le portable le stoppe automatiquement (super = insiste bien pour appeler une fonction de la classe parente)
    isOpen = false
  }
  // Les fonctions start et stop sont déjà présentes
}

val laptop = new Laptop("Dell", "Windows")
laptop.start()
println(f"Allumé : ${laptop.isOn}")
laptop.close() // Appelle la fonction stop() puisque le portable est allumé
println(f"Allumé : ${laptop.isOn}")
Allumé : true
Allumé : false

Le fait de pouvoir utiliser une approche fonctionnelle sur des objets rend le langage Scala extrêmement puissant, à condition d'utiliser correctement et à bon escient les modèles de chaque paradigme.

Quel environnement pour Scala ?

Tout comme la plupart des langages, il est fortement conseillé d'utiliser un IDE. IntelliJ IDEA (dans sa version Community) est sûrement le plus populaire pour coder en Scala, et il dispose de plugins Scala qui permettent de récupérer les sources des différentes versions et de développer très rapidement un projet.


À lire aussi : découvrez notre formation Data Engineer


Il est aussi possible d'utiliser des outils comme sbt (Scala Build Tool) pour développer et package des projets Scala que nous n'aborderons pas dans ce cours.

Puisque Scala s'exécute sur la JVM (Java Virtual Machine), il suffit juste d'avoir une version de développement Java installé sur son PC (Windows, Linux ou Mac) pour commencer directement à coder en Scala. Il n'est pas nécessaire d'avoir une machine puissante, car Scala hérite de la légèreté d'exécution de Java.

Comment se former en Scala ?

La maîtrise du langage Scala demande un investissement en temps, car pour manipuler ce langage, il est nécessaire de comprendre toutes ses spécificités.

Blent propose une formation complète en Scala afin de pouvoir maîtriser les deux paradigmes fonctionnels et objets de ce langage. De nombreux exercices viennent compléter l'apprentissage tout au long du parcours de formation.

Articles similaires

Blog

7 févr. 2024

Data Engineering

Pendant de nombreuses années, le rôle des Data Engineers était de récupérer des données issues de différentes sources, systèmes de stockage et applications tierces et de les centraliser dans un Data Warehouse, dans le but de pouvoir obtenir une vision complète et organisée des données disponibles.

Maxime Jumelle

Maxime Jumelle

CTO & Co-Founder

Lire l'article

Blog

4 déc. 2023

Data Engineering

Pour de nombreuses entreprises, la mise en place et la maintenant de pipelines de données est une étape cruciale pour avoir à disposition d'une vue d'ensemble nette de toutes les données à disposition. Un des challenges quotidien pour les Data Analysts et Data Engineers consiste à s'assurer que ces pipelines de données puissent répondre aux besoins de toutes les équipes d'une entreprise.

Maxime Jumelle

Maxime Jumelle

CTO & Co-Founder

Lire l'article

Blog

14 nov. 2023

Data Engineering

Pour améliorer les opérations commerciales et maintenir la compétitivité, il est essentiel de gérer efficacement les données en entreprise. Cependant, la diversité des sources de données, leur complexité croissante et la façon dont elles sont stockées peuvent rapidement devenir un problème important.

Maxime Jumelle

Maxime Jumelle

CTO & Co-Founder

Lire l'article