Par Maxime Jumelle
CTO & Co-Founder
Publié le 28 févr. 2022
Catégorie Data Engineering
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.
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.
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.
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.
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.
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.
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.
List
, les Stack
ou encore les Vector
.ListSet
ou les HashSet
.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.
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)]\).
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.
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.
Modélisons une première classe qui représente un ordinateur que nous allons nommer Computer
.
brand
(immuable) qui indique la marque de l'ordinateur et os
(mutable) qui indique le système d'exploitation.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) { // ... } // ... }
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.
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.
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.
Vous souhaitez vous former au Data Engineering ?
Articles similaires
7 févr. 2024
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
CTO & Co-Founder
Lire l'article
4 déc. 2023
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
CTO & Co-Founder
Lire l'article
14 nov. 2023
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
CTO & Co-Founder
Lire l'article
60 rue François 1er
75008 Paris
Blent est une plateforme 100% en ligne pour se former aux métiers Tech & Data.
Organisme de formation n°11755985075.
Data Engineering
IA Générative
MLOps
Cloud & DevOps
À propos
Gestion des cookies
© 2024 Blent.ai | Tous droits réservés