Si vous pensez que vos clients sont pénibles, allez donc faire un tour sur Not Always Right.

Passer de Java à Scala

Passer de Java à Scala

 

Scala est l’étoile montante de l’informatique ces derniers temps ! De nombreuses entreprises passent (ou du moins songent à) passer leur application programmée en Java à du Scala.

Pourquoi cet engouement ? Et bien probablement parce que Scala se prend facilement en main, notamment pour les nouveaux développeurs, et qu’il possède une grande richesse au niveau de sa flexibilité et de ses fonctionnalités.

Cependant ce festival de fonctionnalités (conversions implicites et dynamiques par exemple) peut apporter son lot de problèmes. Le langage est très permissif et de bonnes pratiques doivent donc être mises en place pour tirer tout le potentiel du langage sans pour autant que le code ne devienne trop illisible.


Retrouvez le code source de cet article ici : https://github.com/alexandrelanglais/scala-tutorial

Article adapté de la documentation officielle : http://docs.scala-lang.org/tutorials/scala-for-java-programmers.html

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

Introduction à Scala

Scala se compile en bytecode Java et s’exécute donc sur une JVM Java.

Contrairement à Java, Scala intègre les paradigmes de programmation orientée objet mais également ceux de la programmation fonctionnelle avec un typage statique. Il concilie ainsi 2 paradigmes qui sont habituellement opposés.

Pour ceux qui n’ont jamais fait de programmation fonctionnelle (Erlang, OCaml, Haskell…), ce nouveau paradigme demandera une phase d’apprentissage pour bien comprendre les ressources qu’offre Scala.

Hello World !

En tant que développeur Java expérimenté, je vais me passer de la compilation de Scala en ligne de commande pour commencer directement par télécharger l’IDE basée sur Eclipse à l’adresse suivante : http://scala-ide.org/download/sdk.html

On va ensuite créer un nouveau projet Scala et programmer un premier programme : le classique Hello World :

object HelloWorld {
  def main(args: Array[String]) {
    println("Hello world!");
  }
}

Assez ressemblant au java jusqu’à présent : on créé un nouvel Objet du nom de HelloWorld, on définit le point d’entrée main et on utilise la fonction pré-incluse println pour afficher notre Hello World.

Notez que l’on aurait pu également créer une classe Scala de la sorte :

class HelloWorld {
...
}

En utilisant le mot object au lieu de class, on définit en fait un singleton, une classe qui n’a qu’une seule instance. Cette déclaration créé à la fois une classe et une instance, cette dernière étant créée la première fois qu’elle est utilisée.

Autre différence par rapport à Java : la méthode main n’est pas déclarée static : c’est parce que les membres ou fonctions statiques n’existent pas en Scala. A la place, on utilise des object.

Interaction avec Java

Scala étant compilé pour s’exécuter sur une JVM, l’interaction avec Java est très simple : c’est d’ailleurs une des grandes forces de Scala.

Par défaut, toutes les classes du package java.lang sont importées, les autres pouvant être importées explicitement.

import java.util.{Date, Locale}
import java.text.DateFormat
import java.text.DateFormat._

object FrenchDate {
  def main(args: Array[String]) {
    val now = new Date
    val df = getDateInstance(LONG, Locale.FRANCE)
    println(df format now)
  }
}

Les imports de classes se font de la même manière qu’en Java, mais c’est un peu plus puissant :

  • on peut importer plusieurs classes dans même package en les entourant d’accolades
  • l’utilisation de l’underscore _ est équivalent à l’astérisque * pour importer toutes les classes du packages (l’astérisque étant un caractère réservé)
  • l’import DateFormat._ importe donc toutes les fonctions et variables de cette classe, similaire à l’import static de Java

On notera particulièrement la dernière ligne du programme : une méthode ayant un seul argument peut être appelée avec une syntaxe « infix ». On peut soit écrire

df format now

soit

df.format(now)

Enfin, on notera l’absence de points-virgule en fin de lignes.

Tout est Objet

Scala est purement orienté objet, ce qui signifie que tout (absolument tout) est objet, y compris les fonctions et les types primitifs.

Les nombres sont des objets

Les nombres étant des objets, ils ont des méthodes. L’expression suivante

println(1 + 2 * 3 / x)

peut aussi bien être écrite

(1).+(((2).*(3))./(x))

Le signe + est ainsi une fonction du nombre 1, le * une fonction du nombre 2 etc…

Eclipse nous propose des opérateurs sur les nombres

Les fonctions sont des objets

Chose qui peut être surprenante quand on vient du Java (moins si on a fait du C avec les pointeurs de fonctions), les fonctions sont également des objets. C’est à dire qu’elles peuvent être passées en paramètre, stockées dans des variables, ou être des retours d’autres fonctions.

Prenant le programme ci-dessous :

object Timer {
  def oncePerSecond(callback: () => Unit) {
    while (true) { callback(); Thread sleep 1000 }
  }
  def timeFlies() {
    println("time flies like an arrow...")
  }
  def main(args: Array[String]) {
    oncePerSecond(timeFlies)
  }
}

Ici, main appelle la méthode oncePerSecond en lui passant en argument la fonction timeFlies.

Dans oncePerSecond, la déclaration du paramètre en fonction se fait via callback : () => Unit

Cela veut dire que la fonction est référencée dans la variable nommée callback, qu’elle ne prend pas d’argument () et qu’elle ne retourne rien (Unit). Unit étant l’équivalent de void.

Fonctions anonymes

La fonction timeFlies étant utilisée une seule fois, on peut la définir « inline » lors de l’appel à oncePerSecond :

object TimerAnonymous {
  def oncePerSecond(callback: () => Unit) {
    while (true) { callback(); Thread sleep 1000 }
  }
  def main(args: Array[String]) {
    oncePerSecond(() =>
      println("time flies like an arrow..."))
  }
}

La fonction anonyme est reconnaissable du fait du paramètre de oncePerSecond qui commence par =>

Les classes

Les classes en Scala sont déclarées à peu près de la même manière qu’en Java, à ceci près qu’elles peuvent prendre des paramètres. Ces paramètres doivent être passés au constructeur de l’objet.

class Complex(real: Double, imaginary: Double) {
  def re() = real
  def im() = imaginary
}

Cette classe comporte 2 méthodes : re() et im(), qui sont en fait des getters des 2 paramètres de la classe. Il n’est pas nécessaire de déclarer leur type de retour : celui-ci sera déduit par le compilateur par la définition faite des paramètres.

Toutefois, il n’est pas toujours automatique que le compilateur déduise de lui-même le type de retour des méthodes.

Méthodes sans arguments

Les méthodes sans argument peuvent être définies en omettant les parenthèses de celles-ci. Les méthode de l’exemple précédent ne prennent par d’argument, on pourrait donc les définir ainsi

class Complex(real: Double, imaginary: Double) {
  def re = real
  def im = imaginary
}

On pourra par la suite y accéder comme s’il s’agissait de champs publiques de la classes

object ComplexNumbers {
  def main(args: Array[String]) {
    val c = new Complex(1.2, 3.4)
    println("imaginary part: " + c.im)
  }
}

Héritage et surcharge

En Scala, toutes les classes héritent d’une classe parente. Si aucune classe n’est spécifiée, elles héritent implicitement de scala.AnyRef

Pour surcharger une méthode heritée en Scala à l’aide du mot-clé override

On peut par exemple redéfinir la méthode toString de la classe scala.AnyRef

class Complex(real: Double, imaginary: Double) {
  def re = real
  def im = imaginary
  
  override def toString() =
    "" + re + (if (im < 0) "" else "+") + im + "i"
}

Les classes « case » et le pattern matching

En programmation, beaucoup de données sont structurées sous forme d’arbre (un document DOM, un arborescence de processus etc…)

En Scala, il est possible d’utiliser ce qu’on appelle des Case Classe et du Pattern Matching pour représenter un arbre.

Voyons un exemple en réalisant une calculatrice capable de gérer des constants, des additions et des variable. Par exemple, elle devrait pouvoir résoudre 1+2 et (x+x) + (7 +y).

Une représentation pour ce calcul pourrait être un arbre comme ci-dessous :

En Java, on utiliserait une classe abstraite ayant des variables en guise d’opérandes et des fonctions pour les opérateurs.

Dans un langage fonctionnel, on utilise plutôt un type de données algébrique.

Scala offre une solution qui se trouve à mi-chemin avec les case classes. Voici un exemple de déclaration de cet arbre :

abstract class Calcul {
  case class Sum(l: Calcul, r: Calcul) extends Calcul
  case class Var(n: String) extends Calcul
  case class Const(v: Int) extends Calcul
}

Les case class sont un peu particulières dans le sens où :

  • on n’a pas besoin d’employer explicitement le mot-clé new (on peut écrire directement Const(5)
  • les getters sont automatiquement definis
  • les méthodes equals et hashCode sont automatiquement définies, se basant sur la structure des instances et non leur identité
  • une méthode toString par défaut est fournie en forme « code source »
  • les instances de ces classes peuvent être décomposées grâce à du pattern matching (voir plus bas)

Avec ce type de données, on va pouvoir définir des opérations pour le manipuler. On va commencer avec une fonction qui évalue une expression définie dans un environnement Env.

L’environnement Env a pour but d’associer des valeurs aux variables (ici x et y). Par exemple, dans un environnement où l’on assigne la valeur 5 à x, l’opération x + 1 vaudra 6.

Pour représenter un environnement où x -> 5 en Scala, on va écrire :

{ case "x" => 5 }

En fait, un environnement est une fonction qui assigne une valeur à une variable.  Pour définir un environnement où x = 5 et y = 7, on écrira

val env: Environment = { case "x" => 5 case "y" => 7 }

Maintenant que nous avons notre environnement et notre type de données, il est temps d’écrire notre fonction qui évalue l’expression arithmétique. Nous l’appellerons eval

def eval(t: Tree, env: Environment): Int = t match {
  case Var(n)    => env(n)
}

Pour l’instant, il n’y a qu’une seule case classe : Var

Cette case classe permet de récupérer la valeur à une variable dans l’environnement passé en paramètre de la fonction.

On peut écrire le programme ci-dessous pour définir l’expression et afficher la valeur interprétée :

object Calculette extends Calcul {
  
  type Environment = String => Int
  
  def eval(t: Calcul, env: Environment): Int = t match {
    case Var(n)    => env(n)
  }

  def main(args: Array[String]) {
    val exp: Calcul = Var("x")
    val env: Environment = { case "x" => 5 }
    println("Expression: " + exp)
    println("Evaluation with x=5: " + eval(exp, env))
  }
}

L’output sera

Expression: Var(x)
Evaluation with x=5: 5

Une dernière chose : on définit le type Environment comme étant un espace où l’on convertit les String en Int. Ainsi, via le Environment = { case « x » => 5 }, on convertit la chaine x en entier 5.

Agrémentons la fonction eval des méthodes permettant de récupérer les constantes (pour gérer l’opération 7 + y par exemple) et de faire la somme de 2 nombres (variables ou constants)

def eval(t: Calcul, env: Environment): Int = t match {
  case Sum(l, r) => eval(l, env) + eval(r, env)
  case Var(n)    => env(n)
  case Const(v)  => v
}

Tout d’abord le case classe Const : ici rien de spécial, on place juste la valeur de v dans l’environnement.

Ensuite le case classe Sum : ici, on spécifie 2 paramètres : les opérandes (l et r pour left et right). L’opération que l’on va exécuter est une récurrence sur la méthode eval en passant l’opérande de gauche, puis l’opérande de droite, de manière à se retrouver avec 2 entiers (eval retourne un entier).

Puis, on va additionner ces 2 entiers pour compléter la méthode Sum.

La récurrence permet d’imbriquer plusieurs appels à Sum. On peut désormais résoudre l’opération (x + x) + (y + 7) comme ceci :

val exp: Calcul = Sum(Sum(Var("x"),Var("x")),Sum(Const(7),Var("y")))

Ce qui produira l’output suivant :

Expression: Sum(Sum(Var(x),Var(x)),Sum(Const(7),Var(y)))
Evaluation with x=5, y=7: 24

Voici le fichier Calculette.scala au complet :

object Calculette extends Calcul {
  
  type Environment = String => Int
  
  def eval(t: Calcul, env: Environment): Int = t match {
    case Sum(l, r) => eval(l, env) + eval(r, env)
    case Var(n)    => env(n)
    case Const(v)  => v
  }

  def main(args: Array[String]) {
    val exp: Calcul = Sum(Sum(Var("x"),Var("x")),Sum(Const(7),Var("y")))
    val env: Environment = { case "x" => 5 case "y" => 7 }
    println("Expression: " + exp)
    println("Evaluation with x=5, y=7: " + eval(exp, env))
  }
}

Et le type Calcul.scala

abstract class Calcul {
  case class Sum(l: Calcul, r: Calcul) extends Calcul
  case class Var(n: String) extends Calcul
  case class Const(v: Int) extends Calcul
}

Pour rappel :

Retrouvez le code source de cet article ici : https://github.com/alexandrelanglais/scala-tutorial

Article adapté de la documentation officielle : http://docs.scala-lang.org/tutorials/scala-for-java-programmers.html