Introduction à Scala

Introduction à Scala

1. introduction

Dans ce didacticiel, nous allons examinerScala - l'un des principaux langages qui s'exécutent sur la machine virtuelle Java.

Nous commencerons par les principales fonctionnalités du langage telles que les valeurs, les variables, les méthodes et les structures de contrôle. Ensuite, nous explorerons certaines fonctionnalités avancées telles que les fonctions d'ordre supérieur, le curry, les classes, les objets et la correspondance de modèles.

Pour avoir un aperçu des langages JVM, consultez nosquick guide to the JVM Languages

2. Configuration du projet

Dans ce tutoriel, nous utiliserons l'installation standard de Scala à partir dehttps://www.scala-lang.org/download/.

Tout d'abord, ajoutons la dépendance scala-library à notre pom.xml. Cet artefact fournit la bibliothèque standard pour le langage:


    org.scala-lang
    scala-library
    2.12.7

Deuxièmement, ajoutons lesscala-maven-plugin pour compiler, tester, exécuter et documenter le code:


    net.alchim31.maven
    scala-maven-plugin
    3.3.2
    
        
            
                compile
                testCompile
            
        
    

Maven a les derniers artefacts pourscala-lang etscala-maven-plugin.

Enfin, nous utiliserons JUnit pour les tests unitaires.

3. Caractéristiques de base

Dans cette section, nous examinerons les fonctionnalités de base du langage à travers des exemples. Nous utiliserons lesScala interpreter à cette fin.

3.1. Interprète

L'interprète est un shell interactif permettant d'écrire des programmes et des expressions.

Imprimons "hello world" en l'utilisant:

C:\>scala
Welcome to Scala 2.12.6 (Java HotSpot(TM)
 64-Bit Server VM, Java 1.8.0_92).
Type in expressions for evaluation.
Or try :help.

scala> print("Hello World!")
Hello World!
scala>

Ci-dessus, nous commençons l’interprète en tapant «scala» sur la ligne de commande. L'interprète démarre et affiche un message de bienvenue suivi d'une invite.

Ensuite, nous tapons notre expression à cette invite. L'interprète lit l'expression, l'évalue et imprime le résultat. Ensuite, il boucle et affiche à nouveau l'invite.

Comme il fournit un retour immédiat, l'interprète est le moyen le plus simple de se familiariser avec la langue. Par conséquent, utilisons-le pour explorer les fonctionnalités de base du langage: expressions et diverses définitions.

3.2. Expressions

Any computable statement is an expression.

Écrivons quelques expressions et voyons leurs résultats:

scala> 123 + 321
res0: Int = 444

scala> 7 * 6
res1: Int = 42

scala> "Hello, " + "World"
res2: String = Hello, World

scala> "zipZAP" * 3
res3: String = zipZAPzipZAPzipZAP

scala> if (11 % 2 == 0) "even" else "odd"
res4: String = odd

Comme nous pouvons le voir ci-dessus,every expression has a value and a type.

If an expression does not have anything to return, it returns a value of type Unit. Ce type n'a qu'une seule valeur:(). Il est similaire au mot clévoid en Java.

3.3. Définition de la valeur

Le mot-cléval est utilisé pour déclarer des valeurs.

Nous l'utilisons pour nommer le résultat d'une expression:

scala> val pi:Double = 3.14
pi: Double = 3.14

scala> print(pi)
3.14

Cela nous permet de réutiliser le résultat plusieurs fois.

Values are immutable. Par conséquent, nous ne pouvons pas les réaffecter:

scala> pi = 3.1415
:12: error: reassignment to val
       pi = 3.1415
         ^

3.4. Définition de variable

Si nous devons réaffecter une valeur, nous la déclarons comme une variable.

Le mot-clévar  est utilisé pour déclarer des variables:

scala> var radius:Int=3
radius: Int = 3

3.5. Définition de la méthode

Nous définissons des méthodes en utilisant le mot-clédef. Après le mot-clé, nous spécifions le nom de la méthode, les déclarations de paramètres, un séparateur (deux points) et le type de retour. Après cela, nous spécifions un séparateur (=) suivi du corps de la méthode.

Contrairement à Java, nous n'utilisons pas le mot cléreturn pour renvoyer le résultat. Une méthode renvoie la valeur de la dernière expression évaluée.

Écrivons une méthodeavg pour calculer la moyenne de deux nombres:

scala> def avg(x:Double, y:Double):Double = {
  (x + y) / 2
}
avg: (x: Double, y: Double)Double

Ensuite, invoquons cette méthode:

scala> avg(10,20)
res0: Double = 12.5

Si une méthode ne prend aucun paramètre, nous pouvons omettre les parenthèses lors de la définition et de l'appel. De plus, nous pouvons omettre les accolades si le corps n'a qu'une seule expression.

Écrivons une méthode sans paramètrecoinToss qui renvoie aléatoirement "Head" ou "Tail":

scala> def coinToss =  if (Math.random > 0.5) "Head" else "Tail"
coinToss: String

Ensuite, invoquons cette méthode:

scala> println(coinToss)
Tail
scala> println(coinToss)
Head

4. Structures de contrôle

Les structures de contrôle nous permettent de modifier le flux de contrôle dans un programme. Nous avons les structures de contrôle suivantes:

  • Si-autre expression

  • Boucle While et boucle While

  • Pour expression

  • Essayez l'expression

  • Match expression

Contrairement à Java, nous n'avons pas de mots cléscontinue oubreak. Nous avons le mot-cléreturn. Cependant, nous devrions éviter de l'utiliser.

Au lieu de l'instructionswitch, nous avons la correspondance de modèle via l'expression de correspondance. De plus, nous pouvons définir nos propres abstractions de contrôle.

4.1. sinon

La sexpressionif-else est similaire à Java. La partieelse est facultative. Nous pouvons imbriquer plusieurs expressions if-else.

Since it is an expression, it returns a value. Par conséquent, nous l'utilisons comme l'opérateur ternaire (? :) en Java. En fait, la languedoes not have have the ternary operator.

En utilisant if-else, écrivons une méthode pour calculer le plus grand diviseur commun:

def gcd(x: Int, y: Int): Int = {
  if (y == 0) x else gcd(y, x % y)
}

Ensuite, écrivons un test unitaire pour cette méthode:

@Test
def whenGcdCalledWith15and27_then3 = {
  assertEquals(3, gcd(15, 27))
}

4.2. Alors que la boucle

La boucle while a une condition et un corps. Il évalue de manière répétée le corps d'une boucle alors que la condition est vraie. La condition est évaluée au début de chaque itération.

Comme il n'a rien d'utile à renvoyer, il renvoieUnit.

Utilisons la boucle while pour écrire une méthode permettant de calculer le plus grand diviseur commun:

def gcdIter(x: Int, y: Int): Int = {
  var a = x
  var b = y
  while (b > 0) {
    a = a % b
    val t = a
    a = b
    b = t
  }
  a
}

Ensuite, vérifions le résultat:

assertEquals(3, gcdIter(15, 27))

4.3. Faire en boucle

La boucle do while est similaire à la boucle while sauf que la condition de la boucle est évaluée à la fin de la boucle.

En utilisant la boucle do-while, écrivons une méthode pour calculer factorielle:

def factorial(a: Int): Int = {
  var result = 1
  var i = 1
  do {
    result *= i
    i = i + 1
  } while (i <= a)
  result
}

Ensuite, vérifions le résultat:

assertEquals(720, factorial(6))

4.4. Pour l'expression

L'expression for est beaucoup plus polyvalente que la boucle for en Java.

Il peut itérer sur une ou plusieurs collections. De plus, il peut filtrer les éléments et produire de nouvelles collections.

À l'aide de l'expression for, écrivons une méthode pour additionner une plage d'entiers:

def rangeSum(a: Int, b: Int) = {
  var sum = 0
  for (i <- a to b) {
    sum += i
  }
  sum
}

Ici,a to b est une expression de générateur. Il génère une série de valeurs dea àb.

i ← a to b  est un générateur. Il définitasval et lui affecte la série de valeurs produites par l'expression du générateur.

Le corps est exécuté pour chaque valeur de la série.

Ensuite, vérifions le résultat:

assertEquals(55, rangeSum(1, 10))

5. Les fonctions

Scala est un langage fonctionnel. Les fonctions sont ici des valeurs de première classe - nous pouvons les utiliser comme n'importe quel autre type de valeur.

Dans cette section, nous examinerons certains concepts avancés liés aux fonctions: fonctions locales, fonctions d'ordre supérieur, fonctions anonymes et currying.

5.1. Fonctions locales

We can define functions inside functions. Elles sont appelées fonctions imbriquées ou fonctions locales. Similaire aux variables locales,they are visible only within the function they are defined in.

Maintenant, écrivons une méthode pour calculer la puissance à l'aide d'une fonction imbriquée:

def power(x: Int, y:Int): Int = {
  def powNested(i: Int,
                accumulator: Int): Int = {
    if (i <= 0) accumulator
    else powNested(i - 1, x * accumulator)
  }
  powNested(y, 1)
}

Ensuite, vérifions le résultat:

assertEquals(8, power(2, 3))

5.2. Fonctions d'ordre supérieur

Les fonctions étant des valeurs, nous pouvons les transmettre en tant que paramètres à une autre fonction. On peut aussi faire en sorte qu'une fonction retourne une autre fonction.

We refer to functions which operate on functions as higher-order functions. Ils nous permettent de travailler à un niveau plus abstrait. En les utilisant, nous pouvons réduire la duplication de code en écrivant des algorithmes généralisés.

Maintenant, écrivons une fonction d'ordre supérieur pour effectuer une carte et réduire l'opération sur une plage d'entiers:

def mapReduce(r: (Int, Int) => Int,
              i: Int,
              m: Int => Int,
              a: Int, b: Int) = {
  def iter(a: Int, result: Int): Int = {
    if (a > b) {
      result
    } else {
      iter(a + 1, r(m(a), result))
    }
  }
  iter(a, i)
}

Ici,r etm sont des paramètres de typeFunction. En passant différentes fonctions, nous pouvons résoudre divers problèmes, tels que la somme de carrés ou de cubes et la factorielle.

Ensuite, utilisons cette fonction pour écrire une autre fonctionsumSquares qui additionne les carrés des entiers:

@Test
def whenCalledWithSumAndSquare_thenCorrectValue = {
  def square(x: Int) = x * x
  def sum(x: Int, y: Int) = x + y

  def sumSquares(a: Int, b: Int) =
    mapReduce(sum, 0, square, a, b)

  assertEquals(385, sumSquares(1, 10))
}

Ci-dessus, nous pouvons voir quehigher-order functions tend to create many small single-use functions. We can avoid naming them by using anonymous functions.

5.3. Fonctions anonymes

Une fonction anonyme est une expression qui correspond à une fonction. C'est similaire à l'expression lambda en Java.

Réécrivons l'exemple précédent en utilisant des fonctions anonymes:

@Test
def whenCalledWithAnonymousFunctions_thenCorrectValue = {
  def sumSquares(a: Int, b: Int) =
    mapReduce((x, y) => x + y, 0, x => x * x, a, b)
  assertEquals(385, sumSquares(1, 10))
}

Dans cet exemple,mapReduce reçoit deux fonctions anonymes:(x, y) ⇒ x + y etx ⇒ x * x.

Scala can deduce the parameter types from context. Par conséquent, nous omettons le type de paramètres dans ces fonctions.

Cela donne un code plus concis par rapport à l'exemple précédent.

5.4. Fonctions de curry

A curried function takes multiple argument lists, such as def f(x: Int) (y: Int). Il est appliqué en passant plusieurs listes d'arguments, comme dans f (5) (6).

It is evaluated as an invocation of a chain of functions.  Ces fonctions intermédiaires prennent un seul argument et renvoient une fonction.

Nous pouvons également spécifier partiellement des listes d'arguments, telles quef(5).

Maintenant, comprenons cela avec un exemple:

@Test
def whenSumModCalledWith6And10_then10 = {
  // a curried function
  def sum(f : Int => Int)(a : Int, b : Int) : Int =
    if (a > b) 0 else f(a) + sum(f)(a + 1, b)

  // another curried function
  def mod(n : Int)(x : Int) = x % n

  // application of a curried function
  assertEquals(1, mod(5)(6))

  // partial application of curried function
  // trailing underscore is required to
  // make function type explicit
  val sumMod5 = sum(mod(5)) _

  assertEquals(10, sumMod5(6, 10))
}

Au-dessus,sum etmod prennent chacun deux listes d'arguments. On passe les deux listes d'arguments commemod(5)(6). Ceci est évalué comme deux appels de fonction. Tout d'abord,mod(5) est évalué, ce qui renvoie une fonction. Ceci est, à son tour, appelé avec l'argument6. Nous obtenons 1 comme résultat.

Il est possible d'appliquer partiellement les paramètres comme dansmod(5).  Nous obtenons une fonction en conséquence.

De même, dans l'expressionsum(mod(5)) _, nous ne transmettons que le premier argument à la fonctionsum. Par conséquent,sumMod5 est une fonction.

Le trait de soulignement est utilisé comme espace réservé pour les arguments non appliqués. Comme le compilateur ne peut pas déduire qu'un type de fonction est attendu, nous utilisons le soulignement de fin de texte pour rendre le type de retour de fonction explicite.

5.5. Paramètres par nom

Une fonction peut appliquer des paramètres de deux manières différentes - par valeur et par nom - elle n'évalue les arguments par valeur qu'une seule fois au moment de l'appel. En revanche, il évalue les arguments par nom chaque fois qu'ils sont référés. Si l'argument by-name n'est pas utilisé, il n'est pas évalué.

Scala utilise des paramètres par valeur par défaut. Si le type de paramètre est précédé de la flèche (⇒), il passe au paramètre par nom.

Maintenant, utilisons-le pour implémenter la boucle while:

def whileLoop(condition: => Boolean)(body: => Unit): Unit =
  if (condition) {
    body
    whileLoop(condition)(body)
  }

Pour que la fonction ci-dessus fonctionne correctement, les paramètrescondition etbody doivent être évalués à chaque fois qu'ils sont référencés. Par conséquent, nous les définissons en tant que paramètres nommés.

6. Définition de classe

Nous définissons une classe avec le mot-cléclass suivi du nom de la classe.

Après le nom,we can specify primary constructor parameters. Doing so automatically adds members with the same name to the class.

Dans le corps de la classe, nous définissons les membres - valeurs, variables, méthodes, etc. They are public by default unless modified by the private or protected access modifiers.

Nous devons utiliser le mot cléoverride pour remplacer une méthode de la superclasse.

Définissons un employé de classe:

class Employee(val name : String, var salary : Int, annualIncrement : Int = 20) {
  def incrementSalary() : Unit = {
    salary += annualIncrement
  }

  override def toString =
    s"Employee(name=$name, salary=$salary)"
}

Ici, nous spécifions trois paramètres de constructeur -name,salary etannualIncrement.

Puisque nous déclaronsname etsalary avec les mots-clésval etvar, les membres correspondants sont publics. Par contre, nous n'utilisons pas le mot cléval ouvar pour le paramètreannualIncrement. Par conséquent, le membre correspondant est privé. Comme nous spécifions une valeur par défaut pour ce paramètre, nous pouvons l’omettre lors de l’appel du constructeur.

En plus des champs, nous définissons la méthodeincrementSalary. Cette méthode est publique.

Ensuite, écrivons un test unitaire pour cette classe:

@Test
def whenSalaryIncremented_thenCorrectSalary = {
  val employee = new Employee("John Doe", 1000)
  employee.incrementSalary()
  assertEquals(1020, employee.salary)
}

6.1. Classe abstraite

Nous utilisons le mot-cléabstract pour créer un résumé de classe. C'est semblable à celui de Java. Il peut avoir tous les membres qu'une classe ordinaire peut avoir.

De plus, il peut contenir des membres abstraits. Ce sont des membres avec juste déclaration et pas de définition, leur définition étant fournie dans la sous-classe.

De manière similaire à Java, nous ne pouvons pas créer une instance d'une classe abstraite.

Maintenant, illustrons la classe abstraite avec un exemple.

Tout d’abord, créons une classe abstraiteIntSet pour représenter l’ensemble des entiers:

abstract class IntSet {
  // add an element to the set
  def incl(x: Int): IntSet

  // whether an element belongs to the set
  def contains(x: Int): Boolean
}

Ensuite, créons une sous-classe concrèteEmptyIntSet pour représenter l'ensemble vide:

class EmptyIntSet extends IntSet {
  def contains(x : Int) = false
  def incl(x : Int) =
  new NonEmptyIntSet(x, this)
}

Ensuite, une autre sous-classeNonEmptyIntSet représente les ensembles non vides:

class NonEmptyIntSet(val head : Int, val tail : IntSet)
  extends IntSet {

  def contains(x : Int) =
    head == x || (tail contains x)

  def incl(x : Int) =
    if (this contains x) {
      this
    } else {
      new NonEmptyIntSet(x, this)
    }
}

Enfin, écrivons un test unitaire pourNonEmptySet:

@Test
def givenSetOf1To10_whenContains11Called_thenFalse = {
  // Set up a set containing integers 1 to 10.
  val set1To10 = Range(1, 10)
    .foldLeft(new EmptyIntSet() : IntSet) {
        (x, y) => x incl y
    }

  assertFalse(set1To10 contains 11)
}

6.2. Traits

Les traits correspondent aux interfaces Java avec les différences suivantes:

  • capable de sortir d'une classe

  • peut accéder aux membres de la superclasse

  • peut avoir des instructions d'initialisation

Nous les définissons comme nous définissons les classes mais en utilisant le mot-clétrait. En outre, ils peuvent avoir les mêmes membres que les classes abstraites, à l'exception des paramètres de constructeur. En outre, ils sont destinés à être ajoutés à une autre classe en tant que mixin.

Maintenant, illustrons les traits à l'aide d'un exemple.

Tout d'abord, définissons un traitUpperCasePrinter pour nous assurer que la méthodetoString renvoie une valeur en majuscules:

trait UpperCasePrinter {
  override def toString =
    super.toString toUpperCase
}

Ensuite, testons ce trait en l'ajoutant à une classeEmployee:

@Test
def givenEmployeeWithTrait_whenToStringCalled_thenUpper = {
  val employee = new Employee("John Doe", 10) with UpperCasePrinter
  assertEquals("EMPLOYEE(NAME=JOHN DOE, SALARY=10)", employee.toString)
}

Les classes, les objets et les traits peuvent hériter au plus d'une classe, mais d'un nombre quelconque de traits.

7. Définition d'objet

Les objets sont des instances d'une classe. Comme nous l'avons vu dans les exemples précédents, nous créons des objets à partir d'une classe en utilisant le mot-clénew.

Cependant, si une classe ne peut avoir qu'une seule instance, nous devons empêcher la création de plusieurs instances. En Java, nous utilisons le modèle Singleton pour y parvenir.

Pour de tels cas, nous avons une syntaxe concise appelée définition d'objet - similaire à la définition de classe avec une différence. Instead of using the class keyword, we use the object keyword. Cela définit une classe et crée paresseusement son unique instance.

Nous utilisons des définitions d'objets pour implémenter des méthodes utilitaires et des singletons.

Définissons un objetUtils:

object Utils {
  def average(x: Double, y: Double) =
    (x + y) / 2
}

Ici, nous définissons la classeUtils et créons également sa seule instance.

We refer to this sole instance using its nameUtils. Cette instance est créée lors du premier accès.

Nous ne pouvons pas créer une autre instance d'Utils en utilisant le mot-clénew.

Maintenant, écrivons un test unitaire pour l'objetUtils:

assertEquals(15.0, Utils.average(10, 20), 1e-5)

7.1. Objet compagnon et classe compagnon

Si une classe et une définition d'objet ont le même nom, nous les appelons respectivement classe et objet compagnon. Nous devons définir les deux dans le même fichier. Les objets Compagnon peuvent accéder aux membres privés à partir de leur classe compagnon et inversement.

Contrairement à Java,we do not have static members. Au lieu de cela, nous utilisons des objets compagnons pour implémenter des membres statiques.

8. Correspondance de modèle

Pattern matching matches an expression to a sequence of alternatives. Chacun de ces éléments commence par le mot-clécase. Ceci est suivi d'un motif, d'une flèche de séparation (⇒) et d'un certain nombre d'expressions. L'expression est évaluée si le motif correspond.

Nous pouvons construire des modèles à partir de:

  • constructeurs de classe de cas

  • modèle variable

  • le motif générique _

  • littéraux

  • identifiants constants

Les classes de cas facilitent la recherche de motifs sur les objets. Nous ajoutons le mot-clécase lors de la définition d'une classe pour en faire une classe de cas.

Ainsi, la correspondance de modèles est beaucoup plus puissante que l'instruction switch en Java. Pour cette raison, il s'agit d'une fonctionnalité de langage largement utilisée.

Maintenant, écrivons la méthode Fibonacci en utilisant la correspondance de modèle:

def fibonacci(n:Int) : Int = n match {
  case 0 | 1 => 1
  case x if x > 1 =>
    fibonacci (x-1) + fibonacci(x-2)
}

Ensuite, écrivons un test unitaire pour cette méthode:

assertEquals(13, fibonacci(6))

9. Conclusion

Dans ce tutoriel, nous avons introduit le langage Scala et certaines de ses fonctionnalités clés. Comme nous l'avons vu, il fournit un excellent support pour la programmation impérative, fonctionnelle et orientée objet.

Comme d'habitude, le code source complet peut être trouvéover on GitHub.