Ook!, un langage de programmation... primitif.

Passer de Java à Scala – Partie 5 – Les structures de contrôle

Passer de Java à Scala – Partie 5 – Les structures de contrôle

Dans cet article, nous allons voir les structures de contrôle pré-définies dans scala, à savoir if, while, for, try, match.

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

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

Avant-propos

Vous vous demandez surement « Pourquoi revoir ces structures de contrôles alors que la série s’appelle « Passer de Java à Scala » et que toutes ces structures se trouvent de base dans Java ? »

Et bien parce que les structures de contrôles en Scala se comportent d’une manière plus fonctionnelle qu’en Java. Vous allez comprendre pourquoi dès le prochain chapitre : le if

Le if

Le if en Scala se comporte de la même manière qu’en Java à ceci près qu’il est capable de retourner une valeur.

Prenons l’exemple où un nom de fichier est passé en paramètre d’un programme, si le nom de fichier n’est pas spécifié, un nom par défaut est choisi :

object ScalaIf {
  def main(args: Array[String]): Unit = {
    // imperative style : utilisation d'une var
    var fileName = "default.txt"
    if(!args.isEmpty)
      fileName = args(0)
}

On utilise une var que l’on initialise à default.txt, puis on regarde s’il y a un argument en ligne de commande et dans ce cas, on réassigne la variable fileName à la valeur de la ligne de commande.

Ce style n’est pas fonctionnel dans le sens où l’on utilise une var au lieu d’une val. Scala donne la possibilité de ne travailler qu’avec des val dans la plupart des cas pour se rapprocher d’une programmation fonctionnelle.

En tenant compte du fait qu’un if peut renvoyer une valeur (au même titre que l’opérateur ternaire en Java), on peut rééecrire le code comme ceci :

object ScalaIf {
  def main(args: Array[String]): Unit = {
    // functional style : utilisation d'une val
    val fileName2 =
      if(!args.isEmpty) args(0)
      else "default.txt"

    println(fileName2)
  }
}

Beaucoup d’instructions de contrôles renvoient une valeur, ce qui permet d’adopter un style de programmation différent.

Un bon exercice pour se forcer à programmer de manière fonctionnelle est de chercher à remplacer les var par des val.

Les boucles while

La boucle while pour sa part ne renvoie pas de valeur et se comporte de la même manière qu’en Java.

A noter qu’il existe également un do { } while() en Scala.

Le while va souvent de paire avec les var en Scala, son usage est donc peu recommandé. Encore une fois, pour s’entrainer à la programmation fonctionnelle, il est un bon exercice de challenger ses boucles while de la même manière que l’on challenge ses var.

Prenons l’exemple d’une lecture de la saisie utilisateur dans la console. Avec un while en mode impératif, on peut écrire le programme comme suit :

object ScalaWhile extends App {
  var line = ""

  do {
    line = readLine()
    println("Read: " + line)
  } while (line != "")
}

La valeur de la var line est réaffectée à chaque lecture de l’input utilisateur. Pour se passer de ce while, on peut écrire ce programme d’une manière plus fonctionnelle en utilisant la récurrence :

object ScalaWhile extends App {
  def readInput(): Unit = {
    val line = readLine()
    println("Read: " + line)
    if(line != "") readInput()
  }

  readInput()
}

Nous n’avons plus ni var, ni while, et pourtant le comportement du programme sera le même.

Les expressions for

Les for sont le couteau-suisse de l’itération : on peut faire beaucoup de choses avec !

Itérations sur des collections

Pour lister les fichier du répertoire courant, on peut utiliser java.io.File.listFiles() qui retourne une collection de fichiers. L’itération avec un for se fait de la manière suivante :

val files = (new java.io.File(".")).listFiles
for (file <- files)
  println(file)

Sortie :

.\.git
.\.gitignore
.\.idea
.\build.sbt
.\project
.\README.md
.\src
.\target

L’expression file <- files (appelée generator) assigne à une val file le prochain fichier de la collection files (Array[File]). Cette expression fonctionne pour tout type de collection, pas seulement les Array.

Utilisation de ranges

Le classique for(i = 0; i < 5; i++) peut se réaliser simplement grace aux expression to et until :

for(i <- 0 to 5)
  print(i + " ")

println()
for(i <- 0 until 5)
  print(i + " ")

La différence entre to et until ? Avec to, la borne supèrieure sera incluse au for, avec until non. Voici la sortie du programme précédent :

0 1 2 3 4 5 
0 1 2 3 4

Le principal avantage est que i est défini automatiquement en tant que val, on risque ainsi moins d’erreurs de réassignations non voulues en cas de for imbriqués.

Egalement, on risque moins les erreurs classique d’index commençant à 0 ou 1, de i <= array.length -1 etc …

Le filtrage

Il est possible de filtrer les données que l’on récupère via un for directement dans celui-ci. Si par exemple je ne souhaite lister que les dossiers d’un répertoire, je peux écrire ceci :

for (file <- files if file.isDirectory)
  println(file)

En sortie :

.\.git
.\.idea
.\project
.\src
.\target

On peut aussi écrire le code d’une manière plus familière quand on vient d’un langage de style impératif :

for (file <- files)
  if (file.isDirectory)
    println(file)

Cette écriture alternative est possible car dans ce cas précis, for ne renvoie aucune valeur. Nous verrons par la suite un cas dans lequel for renvoie quelque chose et où ce type d’écriture est incompatible avec un retour.

On parle d’ailleurs bien d’expression et non de boucle for car un for renvoie généralement une valeur.

On peut également mettre plusieurs conditions dans un for : imaginons que l’on ne souhaite afficher que les dossiers visibles et pas ceux commençant par un point :

for (file <- files
     if file.isDirectory
     if !file.getName.startsWith(".")
) println(file)

La sortie :

.\project
.\src
.\target

Itérations imbriquées

En rajoutant des générators de type <- , on va obtenir des itérations imbriquées. Imaginons que pour chaque dossier, on veuille parcourir chaque fichier en son seing :

for (folder <- files
      if folder.isDirectory
      if !folder.getName.startsWith(".");
      file <- folder.listFiles()
        if file.isFile
        if !file.getName.startsWith(".")
) println(file)

Il s’agit de reprendre le code précédent et d’y ajouter une seconde itération : file <- folder.listFiles . A cette seconde itération, on peut ajouter des filtres comme précédemment, en l’occurrence on vérifie qu’il s’agit bien d’un fichier et qu’il n’est pas caché.

Il est possible d’ajouter les accolades {} pour plus de lisibilité. Cela permettrait de ne pas mettre le point-virgule avant la 2è itération :

for (folder <- files
     if folder.isDirectory
     if !folder.getName.startsWith(".")
) 
{
  for(file <- folder.listFiles()
      if file.isFile
      if !file.getName.startsWith(".")
  )
     println(file)
}

Utilisation de variables dans l’expression for

Il est possible d’introduire des variables au sein de l’expression for. Par exemple pour stocker le nom du fichier dans une variable fileName, on peut écrire :

// utilisation de variables dans l'expression for
for (folder <- files
      if folder.isDirectory
      if !folder.getName.startsWith(".");
      file <- folder.listFiles();
        fileName = file.getName
        if file.isFile
        if !fileName.startsWith(".")
) println(fileName)

Production d’une collection

Un for peut créer comme résultat un flux qui sera itérable avec un autre for par exemple. Ceci est possible grace au mot-clé yield.

Yield va indiquer quelle données ajouter au flux sortant de la boucle for. Si l’on veut retourner un Array de [File] correspondant aux dossiers visibles du répertoire courant, on peut écrire :

def listVisibleFolders() = {
  for (folder <- files
       if folder.isDirectory
       if !folder.getName.startsWith("."))
    yield folder
}
listVisibleFolders().foreach(folder => println(folder))

Là où en Java on devrait utiliser par exemple un Set<String> et faire des appels à Set#add puis retourner le set, Scala nous permet de générer directement la collection.

A noter que le Yield doit se placer après la clause du for. Dans le code ci-dessus, il n’y pas de corps au for, mais le code suivant par exemple ne compilerait pas :

for (folder <- files
     if folder.isDirectory
     if !folder.getName.startsWith(".")) { // erreur de compilation !
  yield folder
}

La gestion d’exceptions

Le try / catch / finally fonctionne de la même manière qu’en Java. Cependant il y a quelques nuances que nous allons voir dans ce chapitre.

Lancement d’exceptions

La syntaxe est la même qu’en Java. Pour une fonction n’acceptant que les nombres pairs, on peut écrire :

println(evenNumber(2))
println(evenNumber(3))

def evenNumber(n: Int) = {
  if(n % 2 != 0) throw new IllegalArgumentException("n doit être pair")
  else n
}

A noter qu’une exception lancée à également un type : Nothing. Nous en parlerons dans un futur article.

Catcher les exceptions

Le catch d’exception ressemble fort au pattern matching, un genre de switch en Scala, que nous verrons dans le prochain chapitre.

Pour capturer l’exception lancée par le programme précédent, on écrire notre try / catch comme suit :

try {
  val file = new FileReader("inexistant.txt")
} catch {
  case ex: FileNotFoundException => println("Erreur : Fichier non trouvé")
  case ex: IOException => println("Erreur IO")
}

Contrairement à Java, Scala ne nous force pas à déclarer qu’une fonction est susceptible de retourner une expression. Il est possible de le faire avec l’annotation @throws mais ce n’est pas obligatoire.

Le finally

Le finally s’exécute qu’une exception ait été catchée ou non :

try {
  val file = new FileReader("inexistant.txt")
} catch {
  case ex: FileNotFoundException => println("Erreur : Fichier non trouvé")
  case ex: IOException => println("Erreur IO")
} finally {
  println("Cette ligne va s'afficher")
}

Retourner une valeur

Comme beaucoup d’autres structures de contrôle en Scaa, le try/catch peut retourner une valeur si une exception a été levée :

def urlFor(path: String) = {
  try {
    new URL(path)
  } catch {
    case ex: MalformedURLException =>
      println("Erreur : Url malformée")
      new URL("http://www.google.com")
  }
}
println(urlFor("toto://google.com"))
println(urlFor("http://demandeatonton.fr"))

Dans le catch, on retourne une valeur en cas d’exception. L’output sera le suivant :

Erreur : Url malformée
http://www.google.com
http://demandeatonton.fr

L’expression match

En Scala, le match est l’équivalent du switch en Java. Cependant il est plus souvent utilisé avec le mot-clé pattern pour matcher un pattern particulier. Nous verrons cela plus en détail plus loin.

Programmons un traducteur français-anglais qui prendra en ligne de commande le mot à traduire. Bon il saura traduire 3 mots mais suffisant pour illustrer le match :

val firstArg = if(args.length > 0) args(0) else ""

firstArg match {
  case "rouge" => println("red")
  case "vert" => println("green")
  case "bleu" => println("blue")
  case _ => println("meh")
}

Vous remarquerez que contrairement au switch Java, il n’y a pas besoin d’instruction break : le programme n’exécutera pas toutes les branches jusqu’à ce qu’il trouve une correspondance.

Egalement on note l’underscore _ en guise de default. En Scala, l’underscore représente une wildcard (déjà vu lors des imports de packages)

La principale différence avec Java est que le match peut renvoyer une valeur. Ainsi, pour éviter les effets de bords du match précédent (une impression est faite à l’écran), on peut écrire :

val traduction =
  firstArg match {
    case "rouge" => "red"
    case "vert" => "green"
    case "bleu" => "blue"
    case _ => "meh"
  }
println(traduction)

Passer du style impératif au fonctionnel

Il peut être difficile pour un programmeur n’ayant jamais fait de programmation fonctionnelle de passer à ce style.

Pour vous aider à passer d’un style à l’autre, voici les questions que vous pouvez vous poser lorsque vous programmez en Scala :

  • Puis-je remplacer cette var par une val ?
  • Cette fonction créé-t-elle un effet de bord ? (modification d’un variable / d’un objet, impression à l’écran, écriture dans un fichier …)
  • Puis-je supprimer ce while en utilisant la récurrence / un for ?
  • Comment faire sans utiliser break ni continue ?
  • Comment faire ce programme sans utiliser de variable temporaire ?

Prenons l’exemple d’un programme qui affiche une table de multiplication à l’utilisateur.

En style impératif, on pourrait programmer cette table de multiplication ainsi :

object TableMultiplicationImperative extends App {
  var i = 1

  while (i <= 10) {
    var j = 1

    while (j <= 10) {
      val prod = (i * j).toString

      var k = prod.length

      while (k < 4) {
        print(" ")
        k += 1
      }
      print(prod)
      j += 1
    }
    println()
    i += 1
  }
}

L’output serait :

 1   2   3   4   5   6   7   8   9  10
 2   4   6   8  10  12  14  16  18  20
 3   6   9  12  15  18  21  24  27  30
 4   8  12  16  20  24  28  32  36  40
 5  10  15  20  25  30  35  40  45  50
 6  12  18  24  30  36  42  48  54  60
 7  14  21  28  35  42  49  56  63  70
 8  16  24  32  40  48  56  64  72  80
 9  18  27  36  45  54  63  72  81  90
10  20  30  40  50  60  70  80  90 100

On peut noter plusieurs choses dans le code précédent qui nous indique qu’il ne s’agit pas d’un style fonctionnel :

  • utilisation de 3 variables différentes
  • utilisation de 3 boucles while imbriquées
  • effets de bords créés (on affiche à la console plusieurs fois durant le programme)

Essayez de faire l’exercice de créer vous-même cette table de multiplication en adoptant un style fonctionnel !

Si vous souhaitez voir la solution, consultez le code source sur github.

Conclusion

Et bien c’était un gros morceau ! 🙂 j’espère que j’aurai été assez clair dans mes explications.

Dans le prochain article nous parlerons des différents types de fonctions (litteral, anonymes etc…)

Laisser un commentaire