From c0f63f2f5b0f5d4832f46b00cb4e7140f3bf264a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agn=C3=A8s=20Maury?= Date: Wed, 13 May 2020 17:00:01 +0200 Subject: [PATCH 1/2] Traduction de scala-for-java-programmers.md --- _fr/tutorials/scala-for-java-programmers.md | 669 ++++++++++++++++++++ 1 file changed, 669 insertions(+) create mode 100644 _fr/tutorials/scala-for-java-programmers.md diff --git a/_fr/tutorials/scala-for-java-programmers.md b/_fr/tutorials/scala-for-java-programmers.md new file mode 100644 index 0000000000..b63a38f40a --- /dev/null +++ b/_fr/tutorials/scala-for-java-programmers.md @@ -0,0 +1,669 @@ +--- +layout: singlepage-overview +title: Tutoriel Scala pour développeurs Java + +partof: scala-for-java-programmers +language: fr +--- + +Par Michel Schinz and Philipp Haller. + +Traduction et arrangements par Agnès Maury. + +## Introduction + +Ce document présente une introduction rapide au langage Scala et à son compilateur. +Il est destiné aux personnes ayant une expérience de programmation et qui souhaitent +un aperçu de ce qu'ils peuvent faire avec Scala. On part du principe que le lecteur possède +une base de connaissance sur la programmation orientée objet, particulièrement sur Java. + + +## Un premier exemple + +Comme premier exemple, nous utiliserons le programme standard *Hello world*. +Il n'est pas très fascinant, mais il permet de démontrer facilement l'utilisation d'outils Scala +sans avoir une grande connaissance du langage. Voilà à quoi il ressemble : + + object HelloWorld { + def main(args: Array[String]): Unit = { + println("Hello, world!") + } + } + +La structure de ce programme devrait être familière pour les développeurs Java : +il consiste en une méthode appelée `main` qui prend les arguments de la ligne de commande, +une array de String, comme paramètre ; le corps de cette méthode consiste en un simple appel de la méthode +prédéfinie `println` avec le salut amical comme argument. Cette méthode `main` ne retourne pas de valeur. +Pourtant, son type de retour est déclaré comme `Unit`. + +Ce qui est moins familier pour les développeurs Java est la déclaration `object` qui contient la méthode +`main`. Une telle déclaration introduit ce qui est communément connu comme un *objet singleton*, qui est une classe +avec une seule instance. La déclaration ci-dessus déclare à la fois une classe nommée `HelloWorld` +et une instance de cette classe, aussi nommée `HelloWorld`. Cette instance est créée sur demande, c'est-à-dire, +la première fois qu'elle est utilisée. + +Le lecteur avisé a pu remarquer que la méthode `main` n'est pas déclarée en tant que `static`. +C'est parce que les membres statiques (membres ou champs) n'existent pas en Scala. Plutôt que de définir des +membres statiques, le développeur Scala déclarent ces membres dans un objet singleton. + +### Compiler l'exemple + +Pour compiler cet exemple, nous utilisons `scalac`, le compilateur Scala. +`scalac` fonctionne comme la plupart des compilateurs : il prend comme argument un fichier source, +potentiellement certaines options, et produit un ou plusieurs fichiers objets. +Les fichiers objets produits sont des fichiers classes de Java classiques. + +Si nous sauvegardons le programme ci-dessus dans un fichier appelé `HelloWorld.scala`, +nous pouvons le compiler en exécutant la commande suivante (le symbole supérieur `>` représente +l'invité de commandes et ne doit pas être écrit) : + + > scalac HelloWorld.scala + +Cette commande va générer un certain nombre de fichiers class dans le répertoire courant. +L'un d'entre eux s'appellera `HelloWorld.class` et contiendra une classe qui pourra être directement exécutée +en utilisant la commande `scala`, comme décrit dans la section suivante. + +### Exécuter l'exemple + +Une fois compilé, le programme Scala peut être exécuté en utilisant la commande `scala`. +Son utilisation est très similaire à la commande `java` utilisée en pour exécuter les programmes Java, +et qui accepte les mêmes options. L'exemple ci-dessus peut être exécuté en utilisant la commande suivante, +ce qui produit le résultat attendu : + + > scala -classpath . HelloWorld + + Hello, world! + +## Interaction avec Java + +L'une des forces du Scala est qu'il rend très facile l'interaction avec le code Java. +Toutes les classes du paquet `java.lang` sont importées par défaut, alors que les autres +doivent être importées explicitement. + +Regardons un exemple qui démontre ceci. Nous voulons obtenir et formater la date actuelle +par rapport aux conventions utilisées dans un pays spécifique, par exemple la France. + +Les librairies de classes Java définissent des classes utilitaires très puissantes, comme `Date` +et `DateFormat`. Comme Scala interagit avec Java, il n'y a pas besoin d'implémenter +des classes équivalent dans la librairie de classe de Scala --nous pouvons simplement importer +les classes des paquets correspondants de Java: + + import java.util.{Date, Locale} + import java.text.DateFormat._ + + object DateFrancaise { + def main(args: Array[String]): Unit = { + val maintenant = new Date + val df = getDateInstance(LONG, Locale.FRANCE) + println(df format maintenant) + } + } + +Les déclarations d'import de Scala sont très similaires à celle de Java, cependant, +elles sont bien plus puissantes. Plusieurs classes peuvent être importées du même paquet en les plaçant +dans des accolades comme démontré dans la première ligne. Une autre différence notable est de pouvoir +importer tous les noms d'un paquet ou d'une classe en utilisant le symbole underscore (`_`) au lieu de +l'astérisque (`*`). C'est parce que l'astérisque est un identifiant valide en Scala (par exemple pour +un nom de méthode), comme nous le verrons plus tard. + +Par conséquent, la déclaration d'importation dans la seconde ligne importe tous les membres de la classe +`DateFormat`. Cela rend la méthode statique `getDateInstance` et le champ statiques `LONG` +directement visibles. + +Dans la méthode `main`, nous avons tout d'abord créé une instance de la classe Java `Date` +qui contient par défaut la date actuelle. Ensuite, nous définissons un format de date en utilisant la +méthode statique `getDateInstance` que nous avons importée précédemment. Enfin, nous imprimons +la date actuelle selon l'instance de `DateFormat` localisée. Cette dernière ligne montre une +propriété intéressante de la syntaxe Scala. Les méthodes qui ne prennent en entrée qu'un seul argument +peuvent être utilisées avec une syntaxe infixe. C'est-à-dire que l'expression + + df format maintenant + +est juste une autre façon moins verbeuse d'écrire l'expression + + df.format(maintenant) + +Cela peut paraître comme un détail syntaxique mineur, mais il entraîne des conséquences importantes, +dont l'une va être explorée dans la section suivante. + +Pour conclure cette section sur l'intégration avec Java, il faut noter qu'il est possible +d'hériter de classes Java et d'implémenter des interfaces Java directement en Scala. + +## Tout est un object + +Scala est un langage purement orienté objet dans le sens où *tout* est un objet, +y compris les nombres ou les fonctions. Cela diffère du Java dans cet aspect, car Java +distingue les types primitifs (comme `boolean` et `int`) des types référentiels. + +### Les nombres sont des objets + +Étant donné que les nombres sont des objets, ils ont aussi des méthodes. +De fait, une expression arithmétique comme la suivante : + + 1 + 2 * 3 / x + +consiste exclusivement en des appels de méthodes, parce qu il est équivalent à l'expression +suivante, comme nous l'avons vu dans la section précédente : + + 1.+(2.*(3)./(x) + +Cela veut aussi dire que `+`, `*`, etc. sont des identifiants valides en Scala. + +### Les fonctions sont des objets + +Les fonctions sont aussi des objets en Scala. C'est pourquoi il est possible de passer +des fonctions en arguments, de les stocker dans des variables et de les retourner depuis d'autres +fonctions. Cette capacité à manipuler les fonctions en tant que valeurs est l'une des +pierres angulaires d'un paradigme de programmation très intéressant nommé *programmation fonctionnelle*. + +Pour illustrer à quel point il est peut être utile d'utiliser des fonctions en tant que valeurs, +considérons une fonction minuteur qui vise à performer une action toutes les secondes. Comment faire +pour passer au minuteur une action à performer ? En toute logique, comme une fonction. Ce concept de +passer une fonction devrait être familier à beaucoup de développeurs : il est souvent utilisé dans +le code d'interface utilisateur pour enregistrer des fonctions de rappel qui sont invoquées lorsque +certains évènements se produisent. + +Dans le programme suivant, la fonction minuteur est appelée `uneFoisParSeconde` et prend comme argument +une fonction de rappel. Le type de cette fonction est écrit `() => Unit`. C'est le type de toutes les +fonctions qui ne prennent aucun argument et ne renvoie rien (le type `Unit` est similaire à `void` en C/C++). +La principale fonction de ce programme est d'appeler la fonction minuteur avec une fonction de rappel +qui imprime une phrase dans le terminal. Dans d'autres termes, ce programme imprime à l'infini la phrase +"le temps passe comme une flèche". + + object Minuteur { + def uneFoisParSeconde(retour: () => Unit): Unit = { + while (true) { + retour() + Thread sleep 1000 + } + } + + def leTempsPasse(): Unit = { + println("le temps passe comme une flèche") + } + + def main(args: Array[String]): Unit = { + uneFoisParSeconde(leTempsPasse) + } + } + +Notez que pour imprimer la String, nous avons utilisé la méthode prédéfinie `println` au lieu +d'utiliser celle du paquet `System.out`. + +#### Fonctions anonymes + +Bien que ce programme soit facile à comprendre, il peut être affiné un peu plus. +Premièrement, notez que la fonction `leTempsPasse` est définie uniquement dans le but d'être +passée plus tard dans la fonction `uneFoisParSeconde`. Devoir nommer cette fonction qui ne va +être utilisée qu'une fois peut sembler superflu et il serait plus agréable de pouvoir construire +cette fonction juste au moment où elle est passée à `uneFoisParSeconde`. C'est possible en Scala +en utilisant des *fonctions anonymes*, ce qui correspond exactement à ça : des fonctions sans nom. +La version revisitée de notre programme minuteur en utilisant une fonction anonyme à la place de +*leTempsPasse* ressemble à ça : + + object MinuteurAnonyme { + def uneFoisParSeconde(retour: () => Unit): Unit = { + while (true) { + retour() + Thread sleep 1000 + } + } + + def main(args: Array[String]): Unit = { + uneFoisParSeconde( + () => println("le temps passe comme une flèche") + ) + } + } + +La présence d'une fonction anonyme dans cet exemple est reconnaissable par la flèche pointant à droite +`=>` qui sépare la liste des arguments de la fonction de son corps. Dans cet exemple, la liste des +arguments est vide, comme en témoigne la paire de parenthèses vide à gauche de la flèche. Le corps +de cette fonction est le même que celui de `leTempsPasse` décrit plus haut. + +## Classes + +Comme nous l'avons plus tôt, Scala est un langage orienté objet et de ce fait, possède le concept de classe +(pour être plus exact, il existe certains langages orientés objet ne possèdent pas le concept de classe +mais Scala n'en fait pas partie). Les classes en Scala sont déclarées en utilisant une syntaxe proche de +celle de Java. Une différence notable est que les classes en Scala peuvent avoir des paramètres. +Ceci est illustré dans la définition suivante des nombres complexes. + + class Complexe(reel: Double, imaginaire: Double) { + def re() = reel + def im() = imaginaire + } + +La classe `Complexe` prend en entrée deux arguments : la partie réelle et la partie imaginaire du +nombre complexe. Ces arguments peuvent être passé lors de la création d'une instance de `Complexe` comme +ceci : + + new Complexe(1.5, 2.3) + +La classe contient deux méthodes, appelées `re` et `im` qui donnent accès à ces deux parties. + +Il faut noter que le type de retour de ces méthodes n'est pas explicitement donné. Il sera inféré +automatiquement par le compilateur, qui regarde la partie droite de ces méthodes et en déduit que chacune +de ces fonctions renvoie une valeur de type `Double`. + +Le compilateur n'est pas toujours capable d'inférer des types comme il le fait ici et il n'y a +malheureusement aucune règle simple pour savoir dans quel cas il est capable de le faire. En pratique, +ce n'est pas généralement un problème car le compilateur se plaint quand il n'est pas capable d'inférer +un type qui n'a pas été donné explicitement. Une règle simple que les développeurs débutant en Scala +devrait suivre est d'essayer d'omettre les déclarations de type qui semblent être faciles à +déduire et voir si le compilateur ne renvoie pas d'erreur. Après quelques temps, le développeur devrait +avoir un bon idée de quand il peut omettre les types et quand il faut les spécifier explicitement. + +### Les méthodes sans arguments + +Un petit problème des méthodes `re` et `im` est qu'il faut mettre une paire de parenthèses vides après +leur nom pour les appeler, comme démontré dans l'exemple suivant : + + object NombresComplexes { + def main(args: Array[String]): Unit = { + val c = new Complexe(1.2, 3.4) + println("partie imaginaire : " + c.im()) + } + } + +Il serait plus agréable de pouvoir accéder à la partie réelle et imaginaire comme si elles étaient des +champs, sans ajouter une paire de parenthèses vides. C'est parfaitement faisable en Scala, simplement en +les définissant comme des méthodes *sans argument*. De telles méthodes différents des méthodes avec +aucun argument : elles n'ont pas de parenthèses après leur nom, que ce soit dans leur déclaration +ou lors de leur utilisation. Notre classe `Complexe` peut être réécrite de cette façon : + + class Complexe(reel: Double, imaginaire: Double) { + def re = reel + def im = imaginaire + } + + +### Héritage et redéfinition + +Toutes les classes en Scala hérite d'une super classe. Quand aucun super classe n'est spécifiée, +comme dans l'exemple `Complexe` de la section précédente, la classe `scala.AnyRef` est utilisée +implicitement. + +Il est possible de redéfinir les méthodes héritées d'une super classe en Scala. Cependant, il est +obligatoire de spécifier explicitement qu'une méthode en redéfinit une autre en utilisant le +modificateur `override` dans le but d'éviter les redéfinitions accidentelles. Dans notre exemple, +la classe `Complexe` peut être enrichie avec une redéfinition de la méthode `toString` héritée +de la classe `Object`. + + class Complexe(reel: Double, imaginaire: Double) { + def re() = reel + def im() = imaginaire + override def toString() = "" + re + (if (im >= 0) "+" + im + "i" else "") + } + +Nous pouvons alors appeler la méthode `toString` redéfinie comme ci-dessus. + + object NombresComplexes { + def main(args: Array[String]): Unit = { + val c = new Complexe(1.2, 3.4) + println("toString() redéfinie : " + c.toString) + } + } + +## Les case class et le pattern matching + +L'arbre est un type de structure de données qui revient souvent. +Par exemple, les interpréteurs et les compilateurs représentent généralement en interne les programmes +comme des arbres ; les documents XML sont des arbres ; et beaucoup de conteneurs sont basés sur des +arbres, comme les arbres rouge-noire. + +Nous allons maintenant examiner comment de tels arbres sont représentés et manipulés en Scala à travers +d'un petit programme calculatrice. Le but de ce programme est de manipuler des expressions arithmétiques +simples composées de sommes, de constantes numériques et de variables. Deux exemples de telles expressions +sont `1+2` et `(x+x)+(7+y)`. + +Nous devons d'abord décider d'une représentation pour de telles expressions. +La manière la plus naturelle est un arbre où chaque noeud représente une opération (ici, une addition) et +chaque feuille est une valeur (ici des constantes ou variables). + +En Java, un tel arbre serait représenté par une super classe abstraite pour les arbres et une +sous classe concrète pour chaque noeud et feuille. Dans un langage de programmation fonctionnelle, +on utiliserait plutôt un type de donnée algébrique pour faire la même chose. Scala fournit le concept de +*case class* qui est quelque part entre ces deux concepts. Voici comment elles peuvent être utilisées pour +définir le type des arbres pour notre exemple : + + abstract class Arbre + case class Somme(l: Arbre, r: Arbre) extends Arbre + case class Var(n: String) extends Arbre + case class Const(v: Int) extends Arbre + +Le fait que les classes `Somme`, `Var` et `Const` sont définies en tant que case class signifie qu'elles +différent des classes traditionnelles en différents points : + +- le mot clé `new` n'est pas obligatoire lors de la création d'instance de ces classes (c'est-à-dire qu'on + peut écrire `Const(5)` à la place de `new Const(5)`) ; +- les fonctions accesseurs sont automatiquement définies pour les paramètres du constructeur + (c'est-à-dire qu'il est de récupérer la valeur du paramètre du constructeur `v` pour une instance `c` de + la classe `Const` en écrivant tout simplement `c.v`) ; +- une définition par défaut des méthodes `equals` et `hashCode` est fournie, qui se base sur la + *structure* des instances et non pas leur identité ; +- une définition par défaut de la méthode `toString` est fournie et imprime la valeur "à la source" + (par exemple, l'arbre pour l'expression `x+1` s'imprime comme `Somme(Var(x),Const(1))`) ; +- les instances de ces classes peuvent être décomposées avec un *pattern matching* (filtrage de motif) + comme nous le verrons plus bas. + +Maintenant que nous avons défini le type de données pour représenter nos expressions arithmétiques, +il est temps de définir des opérations pour les manipuler. Nous allons commencer avec une fonction +pour évaluer une expression dans un certain *environnement*. Le but de cet environnement est de +donner des valeurs aux variables. Par exemple, l'expression `x+1` évaluée dans un environnement qui +associe la valeur `5` à la variable `x`, écrit `{ x -> 5 }`, donne comme résultat `6`. + +Il faut donc trouver un moyen de représenter ces environnements. Nous pouvons certes utiliser +une sorte de structure de données associatives comme une table de hashage, mais nous pouvons aussi +utiliser directement des fonctions ! Un environnement n'est ni plus ni moins qu'une fonction qui associe +une valeur à une variable. L'environnement `{ x -> 5 }` décrit plus tôt peut être écrit simplement comme +ceci en Scala : + + { case "x" => 5 } + +Cette notation définit une fonction qui, quand on lui donne une String `"x"` en entrée, retourne l'entier +`5` et renvoie une exception dans les autres cas. + +Avant d'écrire la fonction d'évaluation, donnons un nom au type de ces environnements. +Nous pouvons toujours utiliser le `String => Int` pour ces environnements mais cela simplifie +le programme si nous introduisons un nom pour ce type et rendra les modifications futures plus simples. +En Scala, on le réalise avec la notation suivante : + + type Environnement = String => Int + +À partir de maintenant, le type `Environnement` peut être utilisé comme un alias comme +le type des fonctions de `String` à `Int`. + +Maintenant, nous pouvons donner la définition de l'évaluation de fonction. +Théoriquement, c'est très simple : la valeur d'une somme de deux expressions +est tout simplement la somme des valeurs de ces expressions ; la valeur d'une +variable est obtenue directement à partir de l'environnement ; la valeur d'une +constante est la constante elle-même. Pour l'exprimer en Scala, ce n'est pas plus +compliqué : + + def eval(a: Arbre, env: Environnement): Int = a match { + case Somme(l, r) => eval(l, env) + eval(r, env) + case Var(n) => env(n) + case Const(v) => v + } + +Cette fonction d'évaluation fonctionne en effectuant un pattern matching +sur l'arbre `a`. De façon intuitive, la signification de la définition ci-dessus +devrait être claire : + +1. Tout d'abord, il vérifie si l'arbre `a` est une `Somme`. Si c'est le cas, + il relie le sous arbre de gauche à une nouvelle variable appelée `l` et + le sous arbre de gauche à une variable appelée `r`. Ensuite, il traite + l'expression à droite de la flèche : cette expression peut + utiliser (dans notre exemple, c'est le cas) les deux variables `l` et `r` extraites dans le + motif décrit à gauche de la flèche ; +2. Si la première vérification échoue, c'est-à-dire que l'arbre n'est pas une `Somme`, + on continue et on vérifie si `a` est une `Var`. Si c'est le cas, + il relie le nom contenu dans le noeud `Var` à une variable `n` et + il traite l'expression à droite de la flèche ; +3. Si la deuxième vérification échoue, c'est-à-dire que l'arbre n'est ni + une `Somme` ni une `Var`, on vérifie si l'arbre est un `Const`. Si + c'est le cas, il relie la valeur contenue dans le noeud `Const` à une + variable `v` et il traite l'expression à droite de la flèche ; +4. Enfin, si toutes les vérifications échouent, une exception est levée pour signaler + l'échec de l'expression. Dans notre cas, cela pourrait arriver si + d'autres sous classes de `Arbre` étaient déclarées. + +Nous observons que l'idée basique du pattern matching est de faire correspondre +une valeur à une série de motifs et dès qu'un motif correspond, extraire +et nommer les différentes parties de la valeur pour enfin évaluer du +code qui, généralement, utilise ces parties nommées. + +Un développeur orienté objet chevronné pourrait se demander pourquoi nous n'avions pas +défini `eval` comme une *méthode* de la classe `Arbre` et de ces +sous classes. En effet, nous aurions pu le faire, étant donné que Scala autorise +la définition de méthodes dans les case class tout comme dans les classes normales. +Décider d'utiliser un pattern matching ou des méthodes est donc une question de +goût mais a aussi des implications importantes sur l'extensibilité : + +- quand on utilise des méthodes, il est facile d'ajouter un nouveau type de noeud en même temps + qu'une nouvelle sous classe de `Arbre` est définie. Par contre, + ajouter une nouvelle opération pour manipuler un arbre est + fastidieux car il demande de modifier toutes les sous classes de `Arbre` ; +- quand on utilise un pattern matching, la situation est inversée : ajouter un + nouveau type de noeud demande la modification de toutes les fonctions qui effectuent + un pattern matching sur un arbre pour prendre en compte le nouveau noeud. + Par contre, ajouter une nouvelle opération est facile en la définissant + en tant que fonction indépendante. + +Pour explorer plus loin dans le pattern matching, définissons une autre opération +sur les expressions arithmétiques : la dérivée de fonction. Le lecteur doit +garder à l'esprit les règles suivantes par rapport à cette opération : + +1. la dérivée d'une somme est la somme des dérivées ; +2. la dérivée d'une variable `v` est un si `v` est la + variable utilisée pour la dérivation et zéro sinon ; +3. la dérivée d'une constante est zéro. + +Ces règles peut être traduites presque littéralement en du code Scala +pour obtenir la définition suivante : + + def derivee(a: Arbres, v: String): Arbres = a match { + case Somme(l, r) => Somme(derivee(l, v), derivee(r, v)) + case Var(n) if (v == n) => Const(1) + case _ => Const(0) + } + +Cette fonction introduit deux nouveaux concepts reliés au pattern matching. +Premièrement, l'expression `case` pour les variables ont un *garde*, +une expression suivant le mot clé `if`. Ce garde empêche le pattern matching +de réussir à moins que l'expression est vraie. Ici, il est utilisé +pour s'assurer qu'on retourne la constante `1` uniquement si le nom de +la variable se faisant dériver est la même que la variable de dérivation +`v`. La seconde nouvelle fonctionnalité du pattern matching utilisée ici est +le motif *joker*, représentée par `_`, qui est un motif correspondant +n'importe quelle valeur sans lui donner un nom. + +Nous n'avons pas encore exploré l'étendue du pouvoir du pattern matching, mais nous +nous arrêterons ici afin de garder ce document court. Nous voulons toujours +voir comment les deux fonctions ci-dessus fonctionnent dans un exemple réel. Pour se +faire, écrivons une fonction `main` simple qui effectue plusieurs opérations sur l'expression +`(x+x)+(7+y)` : elle évalue tout d'abord sa valeur dans l'environnement +`{ x -> 5, y -> 7 }` puis on la dérive par rapport à `x` et par rapport à `y`. + + def main(args: Array[String]): Unit = { + val exp: Arbre = Somme(Somme(Var("x"),Var("x")),Somme(Const(7),Var("y"))) + val env: Environnement = { case "x" => 5 case "y" => 7 } + println("Expression : " + exp) + println("Évaluation avec x=5, y=7 : " + eval(exp, env)) + println("Dérivée par rapport à x :\n " + derivee(exp, "x")) + println("Dérivée par rapport à y :\n " + derivee(exp, "y")) + } + +Vous devrez envelopper le type `Environnement` et les méthodes`eval`, `derivee` et `main` +dans un objet `Calc` avant de compiler. En exécutant ce programme, on obtient le résultat attendu : + + Expression : Somme(Somme(Var(x),Var(x)),Somme(Const(7),Var(y))) + Évaluation avec x=5, y=7 : 24 + Dérivée par rapport à x : + Somme(Somme(Const(1),Const(1)),Somme(Const(0),Const(0))) + Dérivée par rapport à y : + Somme(Somme(Const(0),Const(0)),Somme(Const(0),Const(1))) + +En examinant la sortie, on voit que le résultat de la dérivée devrait être simplifiée avant +d'être présentée à l'utilisateur. Définir une simplification basique en utilisant +un pattern matching est un problème intéressant (mais curieusement délicat), laissé +comme exercice pour le lecteur. + +## Traits + +Hormis le fait d'hériter du code d'une super classe, une classe Scala peut aussi +importer du code d'un ou de plusieurs *traits*. + +Peut-être que le moyen le plus simple pour un développeur Java de comprendre que ce qu'est +un trait est de le voir comme une interface qui peut aussi contenir du code. En +Scala, quand une classe hérite d'un trait, elle implémente son interface et +hérite de tout le code contenu dans ce trait. + +Notez que depuis Java 8, les interfaces Java peut aussi contenir du code, soit +en utilisant le mot clé `default` soit avec des méthodes statiques. + +Pour s'apercevoir de l'utilité des traits, regardons un exemple classique : +les objets ordonnés. Il est souvent utile de pouvoir comparer entre eux des objets +d'une même classe, par exemple pour les trier. En Java, +les objets qui sont comparables implémentent l'interface `Comparable`. +En Scala, on peut faire un peu mieux qu'en Java en définissant +notre équivalent de `Comparable` en tant que trait, qu'on appellera +`Ord`. + +Quand on compare des objets, six différents prédicats peuvent être utiles : +plus petit, plus petit ou égal, égal, inégal, plus grand, plus grand ou égal. +Cependant, tous les définir est fastidieux, surtout que quatre de ces six +prédicats peuvent être exprimés en utilisant les deux restantes. En effet, +en utilisant les prédicats égal et plus petit (par exemple), on peut +exprimer les autres. En Scala, toutes ces observations peuvent être +capturées dans la déclaration de trait suivante : + + trait Ord { + def < (that: Any): Boolean + def <=(that: Any): Boolean = (this < that) || (this == that) + def > (that: Any): Boolean = !(this <= that) + def >=(that: Any): Boolean = !(this < that) + } + +Cette définition crée à la fois un nouveau type appelé `Ord`, +qui joue un rôle similaire à l'interface Java `Comparable`, et +des implémentations par défaut de trois prédicats par rapport à un +quatrième prédicat abstrait. Les prédicats d'égalité et d'inégalité n'apparaissent pas +ici vu qu'ils sont présents par défaut dans tous les objets. + +Le type `Any` qui est utilisé plus haut est le type +qui est le super type de tous les autres types en Scala. Il peut être vu comme une +version plus générale du type Java `Object`, puisqu'il est aussi un +super type de types basic comme `Int`, `Float`, etc. + +Pour rendre les objets d'une classes comparables, il est alors suffisant de +définir les prédicats qui testent l'égalité et l'infériorité, puis les mixer +dans la classe `Ord` ci-dessus. Comme exemple, définissons une +classe `Date` qui représente les dates dans le calendrier grégorien. Elles +sont composées d'un jour, un mois et une année, que nous allons +représenter avec des entiers. Nous commençons toutefois la définition de la +classe `Date` comme ceci : + + class Date(a: Int, m: Int, j: Int) extends Ord { + def annee = a + def mois = m + def jour = j + override def toString(): String = annee + "-" + mois + "-" + jour + +La partie importante ici est la déclaration `extends Ord` qui +suit le nom de la classe et ses paramètres. Cela veut dire que la +classe `Date` hérite du trait `Ord`. + +Ensuite, nous redéfinissons la méthode `equals`, héritée de `Object`, +pour comparer correctement les dates en comparant leur +champs individuels. L'implémentation par défaut de `equals` n'est pas +utilisable, car en Java, elle compare les objets physiquement. On arrive +à la définition suivante : + + override def equals(that: Any): Boolean = + that.isInstanceOf[Date] && { + val d = that.asInstanceOf[Date] + d.jour == jour && d.mois == mois && d.annee == annee + } + +Cette méthode utilise les méthodes prédéfinies `isInstanceOf` et +`asInstanceOf`. La première méthode, `isInstanceOf` correspond à l'opérateur +Java `instanceof` et retourne true si et seulement si l'objet +sur lequel elle est appliquée est une instance du type donné. +La deuxième, `asInstanceOf`, correspond à l'opérateur de conversion de type : +si l'objet est une instance du type donné, il est vu en tant que tel, +sinon une `ClassCastException` est levée. + +Enfin, la dernière méthode à définir est le prédicat qui test l'infériorité, +comme décrit plus loin. Elle utilise une autre méthode, +`error` du paquet `scala.sys`, qui lève une exception avec le message d'erreur donné. + + def <(that: Any): Boolean = { + if (!that.isInstanceOf[Date]) + sys.error("on ne peut pas comparer " + that + " et une Date") + + val d = that.asInstanceOf[Date] + (annee < d.annee) || + (annee == d.annee && (mois < d.mois || + (mois == d.mois && jour < d.jour))) + } + +Cela complète la définition de la classe `Date`. Les instances de +cette classe peuvent être vues soit comme des dates, soit comme des objets comparables. +De plus, elles définissent les six prédicats de comparaison mentionnés +au-dessus : `equals` et `<` car elles apparaissent directement dans +la définition de la classe `Date`, mais aussi les autres car elles sont +héritées du trait `Ord`. + +Bien sûr, les traits sont utiles dans d'autres situations que celle décrite ici, +mais discuter de leurs applications plus amplement est hors de la +portée de document. + +## Généricité + +La dernière caractéristique de Scala que nous allons explorer dans ce tutoriel est +la généricité. Les développeurs Java devraient être conscients des problèmes +posés par le manque de généricité dans leur langage, une lacune qui +a été compensée avec Java 1.5. + +La généricité est la capacité d'écrire du code paramétrisé par des types. Par +exemple, un développeur qui écrit une librairie pour des listes liées fait face au +problème de décider quel type donner aux éléments de la liste. +Comme cette liste est destinée à être utilisée dans divers contextes, il n'est +pas possible de décider quel type doit avoir les éléments de liste, par exemple, +`Int`. Ce serait complètement arbitraire et excessivement restrictif. + +Les développeurs Java se retrouvent à utiliser `Object`, le super type de +tous les objets. Cependant, cette solution est loin d'être +idéale, puisqu'elle ne marche pas pour les types basiques (`int`, +`long`, `float`, etc.) et cela implique que le développeur +devra faire un certain nombre de conversion de types. + +Scala rend possible la définition de classes (et de méthodes) génériques pour +résoudre ce problème. Examinons ceci au travers d'un exemple d'une +classes conteneur la plus simple possible : une référence, qui peut être +vide ou pointer vers un objet typé. + + class Reference[T] { + private var contenu: T = _ + def set(valeur: T) { contenu = valeur } + def get: T = contenu + } + +La classe `Reference` est paramétrisé par un type appelé `T` +qui est le type de son élément. Ce type est utilisé dans le corps de la +classe en tant que de la variable `contenu`, l'argument de la méthode +`set` et le type de retour de la méthode `get`. + +L'échantillon de code ci-dessus introduits les variables en Scala, ce qui ne devrait pas +demander plus d'explications. Cependant, il est intéressant de voir que +la valeur initiale donnée à la variable est `_` qui représente +une valeur par défaut. Cette valeur par défaut est 0 pour les types numériques, +`false` pour le type `Boolean`, `()` pour le type `Unit` +et `null` pour tous les types d'objet. + +Pour utiliser cette classe `Reference`, il faut spécifier quel type utiliser +pour le type paramètre `T`, le type de l'élément contenu dans la cellule. +Par exemple, pour créer et utiliser une cellule contenant +un entier, on peut écrire : + + object ReferenceEntier { + def main(args: Array[String]): Unit = { + val cellule = new Reference[Int] + cellule.set(13) + println("La référence contient la moitié de " + (cellule.get * 2)) + } + } + +Comme on peut le voir dans l'exemple, il n'est pas nécessaire de convertir la valeur +retournée par la méthode `get` avant de pouvoir l'utiliser en tant qu'entier. Il +n'est pas possible de stocker autre chose d'un entier dans cette +cellule particulière, puisqu'elle a été déclarée comme portant un entier. + +## Conclusion + +Ce document donne un rapide aperçu du langage Scala et +présente quelques exemples basiques. Le développeur intéressé peut poursuivre sa lecture, +par exemple, en lisant le *[Tour of Scala](https://docs.scala-lang.org/tour/tour-of-scala.html)* +(document en anglais) et consulter la *spécification du langage Scala* si nécessaire. From 45795ee302cd8e6b5739fdcea9df4e30ea943bfa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agn=C3=A8s=20Maury?= Date: Thu, 11 Jun 2020 11:32:59 +0200 Subject: [PATCH 2/2] Prise en compte des commentaires de la review --- _fr/tutorials/scala-for-java-programmers.md | 66 ++++++++++----------- 1 file changed, 32 insertions(+), 34 deletions(-) diff --git a/_fr/tutorials/scala-for-java-programmers.md b/_fr/tutorials/scala-for-java-programmers.md index b63a38f40a..b89649f1fa 100644 --- a/_fr/tutorials/scala-for-java-programmers.md +++ b/_fr/tutorials/scala-for-java-programmers.md @@ -15,14 +15,14 @@ Traduction et arrangements par Agnès Maury. Ce document présente une introduction rapide au langage Scala et à son compilateur. Il est destiné aux personnes ayant une expérience de programmation et qui souhaitent un aperçu de ce qu'ils peuvent faire avec Scala. On part du principe que le lecteur possède -une base de connaissance sur la programmation orientée objet, particulièrement sur Java. +des connaissances de base sur la programmation orientée objet, particulièrement sur Java. ## Un premier exemple -Comme premier exemple, nous utiliserons le programme standard *Hello world*. -Il n'est pas très fascinant, mais il permet de démontrer facilement l'utilisation d'outils Scala -sans avoir une grande connaissance du langage. Voilà à quoi il ressemble : +Commençons par écrire le célèbre programme *Hello world*. +Bien que simple, il permet de découvrir plusieurs fonctionnalités du language +avec peu de de connaissance préalable de Scala. Voilà à quoi il ressemble : object HelloWorld { def main(args: Array[String]): Unit = { @@ -44,7 +44,7 @@ la première fois qu'elle est utilisée. Le lecteur avisé a pu remarquer que la méthode `main` n'est pas déclarée en tant que `static`. C'est parce que les membres statiques (membres ou champs) n'existent pas en Scala. Plutôt que de définir des -membres statiques, le développeur Scala déclarent ces membres dans un objet singleton. +membres statiques, le développeur Scala déclare ces membres dans un objet singleton. ### Compiler l'exemple @@ -66,7 +66,7 @@ en utilisant la commande `scala`, comme décrit dans la section suivante. ### Exécuter l'exemple Une fois compilé, le programme Scala peut être exécuté en utilisant la commande `scala`. -Son utilisation est très similaire à la commande `java` utilisée en pour exécuter les programmes Java, +Son utilisation est très similaire à la commande `java` utilisée pour exécuter les programmes Java, et qui accepte les mêmes options. L'exemple ci-dessus peut être exécuté en utilisant la commande suivante, ce qui produit le résultat attendu : @@ -80,13 +80,12 @@ L'une des forces du Scala est qu'il rend très facile l'interaction avec le code Toutes les classes du paquet `java.lang` sont importées par défaut, alors que les autres doivent être importées explicitement. -Regardons un exemple qui démontre ceci. Nous voulons obtenir et formater la date actuelle +Prenons l'exemple suivant. Nous voulons obtenir et formater la date actuelle par rapport aux conventions utilisées dans un pays spécifique, par exemple la France. Les librairies de classes Java définissent des classes utilitaires très puissantes, comme `Date` -et `DateFormat`. Comme Scala interagit avec Java, il n'y a pas besoin d'implémenter -des classes équivalent dans la librairie de classe de Scala --nous pouvons simplement importer -les classes des paquets correspondants de Java: +et `DateFormat`. Comme Scala interagit avec Java, il n'y a pas besoin de ré-implémenter ces classes en Scala +--nous pouvons simplement importer les classes des paquets correspondants de Java : import java.util.{Date, Locale} import java.text.DateFormat._ @@ -129,7 +128,7 @@ dont l'une va être explorée dans la section suivante. Pour conclure cette section sur l'intégration avec Java, il faut noter qu'il est possible d'hériter de classes Java et d'implémenter des interfaces Java directement en Scala. -## Tout est un object +## Tout est objet Scala est un langage purement orienté objet dans le sens où *tout* est un objet, y compris les nombres ou les fonctions. Cela diffère du Java dans cet aspect, car Java @@ -187,7 +186,7 @@ qui imprime une phrase dans le terminal. Dans d'autres termes, ce programme impr } } -Notez que pour imprimer la String, nous avons utilisé la méthode prédéfinie `println` au lieu +Notez que pour imprimer la String, nous utilisons la méthode prédéfinie `println` au lieu d'utiliser celle du paquet `System.out`. #### Fonctions anonymes @@ -223,8 +222,8 @@ de cette fonction est le même que celui de `leTempsPasse` décrit plus haut. ## Classes -Comme nous l'avons plus tôt, Scala est un langage orienté objet et de ce fait, possède le concept de classe -(pour être plus exact, il existe certains langages orientés objet ne possèdent pas le concept de classe +Comme nous l'avons vu plus tôt, Scala est un langage orienté objet et de ce fait, possède le concept de classe +(pour être plus exact, il existe certains langages orientés objet qui ne possèdent pas le concept de classe mais Scala n'en fait pas partie). Les classes en Scala sont déclarées en utilisant une syntaxe proche de celle de Java. Une différence notable est que les classes en Scala peuvent avoir des paramètres. Ceci est illustré dans la définition suivante des nombres complexes. @@ -235,7 +234,7 @@ Ceci est illustré dans la définition suivante des nombres complexes. } La classe `Complexe` prend en entrée deux arguments : la partie réelle et la partie imaginaire du -nombre complexe. Ces arguments peuvent être passé lors de la création d'une instance de `Complexe` comme +nombre complexe. Ces arguments peuvent être passés lors de la création d'une instance de `Complexe` comme ceci : new Complexe(1.5, 2.3) @@ -250,9 +249,9 @@ Le compilateur n'est pas toujours capable d'inférer des types comme il le fait malheureusement aucune règle simple pour savoir dans quel cas il est capable de le faire. En pratique, ce n'est pas généralement un problème car le compilateur se plaint quand il n'est pas capable d'inférer un type qui n'a pas été donné explicitement. Une règle simple que les développeurs débutant en Scala -devrait suivre est d'essayer d'omettre les déclarations de type qui semblent être faciles à +devraient suivre est d'essayer d'omettre les déclarations de type qui semblent être faciles à déduire et voir si le compilateur ne renvoie pas d'erreur. Après quelques temps, le développeur devrait -avoir un bon idée de quand il peut omettre les types et quand il faut les spécifier explicitement. +avoir une bonne idée de quand il peut omettre les types et quand il faut les spécifier explicitement. ### Les méthodes sans arguments @@ -280,7 +279,7 @@ ou lors de leur utilisation. Notre classe `Complexe` peut être réécrite de ce ### Héritage et redéfinition -Toutes les classes en Scala hérite d'une super classe. Quand aucun super classe n'est spécifiée, +Toutes les classes en Scala héritent d'une super classe. Quand aucune super classe n'est spécifiée, comme dans l'exemple `Complexe` de la section précédente, la classe `scala.AnyRef` est utilisée implicitement. @@ -310,10 +309,10 @@ Nous pouvons alors appeler la méthode `toString` redéfinie comme ci-dessus. L'arbre est un type de structure de données qui revient souvent. Par exemple, les interpréteurs et les compilateurs représentent généralement en interne les programmes comme des arbres ; les documents XML sont des arbres ; et beaucoup de conteneurs sont basés sur des -arbres, comme les arbres rouge-noire. +arbres, comme les arbres bicolores. Nous allons maintenant examiner comment de tels arbres sont représentés et manipulés en Scala à travers -d'un petit programme calculatrice. Le but de ce programme est de manipuler des expressions arithmétiques +d'un petit programme de calculatrice. Le but de ce programme est de manipuler des expressions arithmétiques simples composées de sommes, de constantes numériques et de variables. Deux exemples de telles expressions sont `1+2` et `(x+x)+(7+y)`. @@ -338,13 +337,13 @@ différent des classes traditionnelles en différents points : - le mot clé `new` n'est pas obligatoire lors de la création d'instance de ces classes (c'est-à-dire qu'on peut écrire `Const(5)` à la place de `new Const(5)`) ; - les fonctions accesseurs sont automatiquement définies pour les paramètres du constructeur - (c'est-à-dire qu'il est de récupérer la valeur du paramètre du constructeur `v` pour une instance `c` de + (c'est-à-dire qu'il est possible de récupérer la valeur du paramètre du constructeur `v` pour une instance `c` de la classe `Const` en écrivant tout simplement `c.v`) ; - une définition par défaut des méthodes `equals` et `hashCode` est fournie, qui se base sur la *structure* des instances et non pas leur identité ; - une définition par défaut de la méthode `toString` est fournie et imprime la valeur "à la source" (par exemple, l'arbre pour l'expression `x+1` s'imprime comme `Somme(Var(x),Const(1))`) ; -- les instances de ces classes peuvent être décomposées avec un *pattern matching* (filtrage de motif) +- les instances de ces classes peuvent être décomposées avec un *pattern matching* (filtrage par motif) comme nous le verrons plus bas. Maintenant que nous avons défini le type de données pour représenter nos expressions arithmétiques, @@ -436,7 +435,7 @@ sur les expressions arithmétiques : la dérivée de fonction. Le lecteur doit garder à l'esprit les règles suivantes par rapport à cette opération : 1. la dérivée d'une somme est la somme des dérivées ; -2. la dérivée d'une variable `v` est un si `v` est la +2. la dérivée d'une variable `v` est 1 si `v` est égale la variable utilisée pour la dérivation et zéro sinon ; 3. la dérivée d'une constante est zéro. @@ -450,13 +449,13 @@ pour obtenir la définition suivante : } Cette fonction introduit deux nouveaux concepts reliés au pattern matching. -Premièrement, l'expression `case` pour les variables ont un *garde*, -une expression suivant le mot clé `if`. Ce garde empêche le pattern matching -de réussir à moins que l'expression est vraie. Ici, il est utilisé + +Premièrement, l'expression `case` qui peut être utilisé avec un *garde* qui suit le mot clé `if`. +Ce garde empêche le pattern matching de réussir à moins que l'expression soit vraie. Ici, il est utilisé pour s'assurer qu'on retourne la constante `1` uniquement si le nom de la variable se faisant dériver est la même que la variable de dérivation `v`. La seconde nouvelle fonctionnalité du pattern matching utilisée ici est -le motif *joker*, représentée par `_`, qui est un motif correspondant +le motif *joker*, représenté par `_`, qui est un motif correspondant à n'importe quelle valeur sans lui donner un nom. Nous n'avons pas encore exploré l'étendue du pouvoir du pattern matching, mais nous @@ -495,8 +494,8 @@ comme exercice pour le lecteur. Hormis le fait d'hériter du code d'une super classe, une classe Scala peut aussi importer du code d'un ou de plusieurs *traits*. -Peut-être que le moyen le plus simple pour un développeur Java de comprendre que ce qu'est -un trait est de le voir comme une interface qui peut aussi contenir du code. En +Peut-être que le moyen le plus simple pour un développeur Java de comprendre les traits +est de le voir comme une interface qui peut aussi contenir du code. En Scala, quand une classe hérite d'un trait, elle implémente son interface et hérite de tout le code contenu dans ce trait. @@ -575,7 +574,7 @@ La deuxième, `asInstanceOf`, correspond à l'opérateur de conversion de type : si l'objet est une instance du type donné, il est vu en tant que tel, sinon une `ClassCastException` est levée. -Enfin, la dernière méthode à définir est le prédicat qui test l'infériorité, +Enfin, la dernière méthode à définir est le prédicat qui teste l'infériorité comme décrit plus loin. Elle utilise une autre méthode, `error` du paquet `scala.sys`, qui lève une exception avec le message d'erreur donné. @@ -592,9 +591,8 @@ comme décrit plus loin. Elle utilise une autre méthode, Cela complète la définition de la classe `Date`. Les instances de cette classe peuvent être vues soit comme des dates, soit comme des objets comparables. De plus, elles définissent les six prédicats de comparaison mentionnés -au-dessus : `equals` et `<` car elles apparaissent directement dans -la définition de la classe `Date`, mais aussi les autres car elles sont -héritées du trait `Ord`. +ci-dessus : `equals` et `<` car elles apparaissent directement dans +la définition de la classe `Date`, ainsi que les autres qui sont directement héritées du trait `Ord`. Bien sûr, les traits sont utiles dans d'autres situations que celle décrite ici, mais discuter de leurs applications plus amplement est hors de la @@ -636,7 +634,7 @@ qui est le type de son élément. Ce type est utilisé dans le corps de la classe en tant que de la variable `contenu`, l'argument de la méthode `set` et le type de retour de la méthode `get`. -L'échantillon de code ci-dessus introduits les variables en Scala, ce qui ne devrait pas +L'échantillon de code ci-dessus introduit les variables en Scala, ce qui ne devrait pas demander plus d'explications. Cependant, il est intéressant de voir que la valeur initiale donnée à la variable est `_` qui représente une valeur par défaut. Cette valeur par défaut est 0 pour les types numériques,