Retrouvez sur cette page une cheat sheet d'enfer pour Scala

Passer de Java à Scala – Partie 6 – Fonctions et clôtures

Passer de Java à Scala – Partie 6 – Fonctions et clôtures

Dans cet article, nous allons étudier en détail les possibilités offertes par les fonctions en Scala, ainsi que les clôtures.

Retrouvez le code source des exemples ici : https://github.com/alexandrelanglais/scala-tutorial-6

Retrouvez tous les articles de la série « Passer de Java à Scala »

Les méthodes

On appelle méthode une fonction qui appartient à un objet. Prenons le code suivant qui lit un fichier et en extrait les lignes les plus longues :

object LongLines {
  def processFile(filename: String, width: Int) {
    val source = Source.fromFile(filename)
    for (line <- source.getLines())
      processLine(filename, width, line)
  }

  private def processLine(filename: String, width: Int, line: String) {
    if (line.length > width)
      println(filename + ": " + line.trim)

  }
}

Pour appeler cette méthode, créons un autre objet qui servira de point d’entrée au programme :

object FindLongLines extends App {
  val fileName = "src/main/scala/LongLines.scala"

  LongLines.processFile(fileName, 50)
}

On voit que le comportement est assez similaire à Java : un objet étant un singleton, l’appel à la méthode processFile ressemble fort à un appel d’une méthode statique.

Scala possède d’autres manières de définir des fonctions que nous allons voir dans la suite de cet article.

Les fonctions locales

En Scala, il est possible de créer des fonctions à l’intérieur d’autres fonctions. On garde ainsi un code découpé pour que chaque fonction fasse ce qu’elle a à faire et rien de plus.

La méthode privée processLine de l’exemple précédent peut très bien devenir une fonction locale de processFile étant donné que seule processFile l’utilise.

object LongLinesWithLocalFunction {
  def processFile(filename: String, width: Int) {
    val source = Source.fromFile(filename)
    for (line <- source.getLines())
      processLine(filename)

    def processLine(line: String) {
      if (line.length > width)
        println(filename + ": " + line.trim)
    }
  }
}

Les fonctions locales ayant un scope réduit à la fonction qui l’englobe, il n’est pas nécessaire de définir un accesseur private pour protéger leur visibilité.

Enfin, les fonctions locales ont accès aux variables appartenant au scope de la fonction appelante. On n’a donc plus besoin de passer les arguments fileName et width à processLine.

Fonctions de première classe

Scala possède ce que l’on appelle des « first-class functions », ou fonctions de première classe, ou encore des « function literals ».

Ces fonctions sont des fonctions sans nom que l’on peut définir et utiliser par la suite comme des valeurs. Un peu comme des fonctions anonymes, elles sont utiles quand on veut passer une fonction en argument d’une autre fonction sans vouloir déclarer explicitement une autre fonction.

Une fonction littérale s’écrit comme ceci :

(x: Int) => x + 1

Avec l’opérateur =>, on définit que la partie droite transforme la partie gauche comme indiqué dans la partie droite.

Les fonctions sont des objets, on peut donc les stocker dans des variables et appeler ces variables. Par exemple :

scala> val increase = (x: Int) => x + 1
increase: Int => Int = $$Lambda$1143/714721945@39f0c343

scala> increase(5)
res2: Int = 6

La fonction anonyme (x: Int) => x + 1 est stockée dans la variable increase. Appeler increase retour la valeur passée en paramètre + 1.

A noter que l’on peut faire faire plusieurs choses à une fonction anonyme :

scala> var increase = (x: Int) => {
     | println("Appel de increase")
     | x + 1
     | }
increase: Int => Int = $$Lambda$1177/1111460467@1aedf08d

scala> increase(5)
Appel de increase
res0: Int = 6

Nous avons déjà utilisé ce genre de fonction auparavant car beaucoup de méthodes Scala prennent des fonctions en argument. Par exemple le foreach sur une collection :

scala> val l = List(1, 2, 3, 4)
l: List[Int] = List(1, 2, 3, 4)

scala> l.foreach((x: Int) => println(x))
1
2
3
4

On retrouve bien en paramètre de notre foreach le même format que la définition de notre fonction anonyme, même si usuellement, on ne spécifie pas le type de x car il est auto déterminé par le compilateur.

En effet, on aura plutôt tendance à écrire :

scala> l.foreach(x => println(x))

La syntaxe placeholder

On peut raccourcir encore plus l’expression précédente : en effet, en utilisant l’underscore _ autant de fois qu’il y a de paramètres définis dans notre fonction anonyme, on peut alors écrire :

scala> l.foreach(println(_))
1
2
3
4

On peut voir le _ comme un blanc à remplir, en l’occurence il sera rempli par les paramètres de la fonction anonyme.

Imaginons que l’on souhaite écrire une fonction anonyme qui additionne 2 nombres passés en paramètres. L’utilisation de placeholders est nécessaire comme dans l’exemple ci-dessous :

scala> val b = (x: Int) + (y: Int)
<console>:11: error: not found: value x
       val b = (x: Int) + (y: Int)
                ^
<console>:11: error: not found: value y
       val b = (x: Int) + (y: Int)

Sinon le compilateur va considérer que l’on se réfère à des variables existantes (x et y) mais non trouvées. Pour que cette fonction compile, il faut utiliser des _ :

scala> val f = (_: Int) + (_: Int)
f: (Int, Int) => Int = $$Lambda$1202/1909198389@3b6c740b

scala> f(2, 4)
res4: Int = 6

Notez que l’on est obligés de spécifier les types des arguments au compilateur, sinon celui-ci n’a pas assez d’informations pour créer la fonction :

scala> val g = _ + _
<console>:11: error: missing parameter type for expanded function ((x$1: <error>, x$2) => x$1.$plus(x$2))
       val g = _ + _
               ^
<console>:11: error: missing parameter type for expanded function ((x$1: <error>, x$2: <error>) => x$1.$plus(x$2))
       val g = _ + _
                   ^

Fonctions partiellement appliquées

Les exemples précédents remplaçaient l’underscore _ par des valeurs de paramètres. Mais il est également possible de remplacer toute une liste de paramètres par un underscore. Par exemple, au lieu d’écrire foreach(println(_)), on peut écrire foreach(println _) et dans ce cas, _ n’est pas remplacé par juste un paramètre mais par toute une liste de paramètres.

En utilisant l’underscore de cette manière, on dit qu’on utilise une fonction partiellement appliquée.

En Scala, quand on passe tous les arguments nécessaires à une fonction, on dit qu’on applique ces arguments à cette fonction.

En prenant l’exemple suivant :

scala> def sum(a: Int, b:Int, c:Int) = a + b + c
sum: (a: Int, b: Int, c: Int)Int

scala> sum(1, 2, 3)
res5: Int = 6

On applique les arguments 1, 2 et 3 à la fonction sum.

Les fonctions partiellement appliquées ne renseignent pas tous les arguments nécessaires à l’exécution de la fonction mais une liste d’arguments potentiels.

Par exemple, on peut écrire une fonction partiellement appliquée implicant sum comme ceci :

scala> val a = sum _
a: (Int, Int, Int) => Int = $$Lambda$1225/849280097@32f45e15

Ici, le compilateur a défini comme étant une référence de la fonction sum à laquelle les arguments ne sont pas spécifiés, on doit donc passer 3 arguments . En appelant a, sum sera appelée :

scala> a(1, 2, 3)
res6: Int = 6

Maintenant imaginons que l’on souhaite toujours appeler sum avec les valeur a = 1 et c = 3. On peut alors écrire une nouvelle référence à sum comme ceci :

scala> val b = sum(1, _:Int, 3)
b: Int => Int = $$Lambda$1226/499302716@579ee82

scala> b(2)
res7: Int = 6

On peut appeler maintenant la fonction sum (enfin b) avec un seul argument, les 2 autres ayant été spécifiés et constants.

Les clôtures

Jusqu’ici, les fonctions définies ont fait références aux paramètres passés. Mais imaginons que nous voulions écrire :

(x: Int) => x + more
<console>:12: error: not found: value more
       (x: Int) => x + more

more est considérée comme une variable libre, contrairement à x qui elle est une variable liée à la fonction.

Pour pouvoir écrire la fonction anonyme ci-dessus, il faut auparavant définir more :

scala> var more = 10
more: Int = 10

scala> val addMore = (x: Int) => x + more
addMore: Int => Int = $$Lambda$1245/2057710224@23ee31b8

La variable faisant référence à la fonction anonyme (addMore) et sa variable utilisée (more) est appelée une clôture. C’est à dire qu’elle a « enfermé » la référence à ses variables libres (more) dans sa définition.

Si maintenant on change la valeur de more :

scala> more = 2000
more: Int = 2000

scala> addMore(1)
res1: Int = 2001

Scala continue d’interpréter la valeur de more bien que celle-ci ait été enfermée dans la fonction anonyme.

Le contraire est aussi vrai : Scala voit les valeurs qui changent en dehors de la clôture. Si on prend l’exemple suivant

scala> val numbers = List(5, -4, 3, 8, -1)
numbers: List[Int] = List(5, -4, 3, 8, -1)

scala> var sum = 0
sum: Int = 0

scala> numbers.foreach(sum += _)

scala> sum
res3: Int = 11

La variable sum qui se trouve en-dehors de la clôture sum += _ est bien accessible et modifiable par le code interne de la clôture.

Les différentes formes d’appels aux fonctions

Comme les fonctions sont un élément central de Scala, certaines formes d’appels ont été ajoutées au langage pour répondre à des besoins particuliers.

Paramètres répétés

Il est possible de spécifier que le dernier argument d’une fonction peut être répété (à la manière des … en Java). Ceci se fait grace à l’astérisque * :

scala> def echo(args: String*) = args.foreach(println)
echo: (args: String*)Unit

scala> echo("hello")
hello

scala> echo ("hello", "world")
hello
world

Arguments nommés

Il est possible de passer des paramètre dans un ordre différent que celui spécifié dans la fonction si l’on nomme les arguments :

scala> def speed(distance: Float, time: Float): Float = distance / time
speed: (distance: Float, time: Float)Float

scala> speed(100, 2)
res7: Float = 50.0

scala> speed(time = 2, distance = 100)
res8: Float = 50.0

Valeur par défaut des paramètres

Pour spécifier une valeur par défaut à un paramètre en Scala, il suffit d’utiliser l’opérateur = dans la déclaration du paramètre de la fonction :

scala> def say(message: String = "Hello!") = println(message)
say: (message: String)Unit

scala> say()
Hello!

scala> say("Bonjour")
Bonjour

Le passage d’un paramètre à la fonction est alors optionnel.

Conclusion

Dans cet article nous avons vu beaucoup de choses concernant les fonctions : locales, anonymes, valeurs de fonctions, fonctions partiellement appliquées… De quoi reconnaitre beaucoup de code Scala lorsque vous en lirez !

Dans le prochain article, nous verrons comment abstraire nos contrôles grâce à tous les mécanismes que nous venons de voir.

One thought on “Passer de Java à Scala – Partie 6 – Fonctions et clôtures

Laisser un commentaire