Archives pour la catégorie Informatique

Nommer les variables et les méthodes

Beaucoup de développeurs ont du mal à donner un nom correct à leurs variables, fonctions et méthodes. Il n’y consacre qu’une seconde à chaque fois qu’il faut en créer une, pensant que le sujet est sans grande importance. C’est au contraire capital.

Il y a un adage qui dit : « Il n’y a que deux choses difficiles en informatique : l’invalidation d’un cache et nommer les choses ». Il y a aussi une variation humoristique, qui dit que les deux choses les plus compliquées en informatique sont : l’invalidation d’un cache, nommer les choses et l’erreur de type « off-by-one ».

Je vais ici expliquer ma vision de comment nommer les choses, qui doit correspondre à ce qu’on trouve ailleurs comme recommandation.

La cohérence

Le premier principe pour nommer les choses dans un projet est la cohérence. Il faut définir des règles et les appliquer partout de la même manière. Ce qu’il faut, c’est qu’on puisse deviner le nom d’une variable ou d’une méthode, sans avoir à aller vérifier à chaque fois à l’endroit où elle a été définie.

Cohérence dans la casse

Il y a plusieurs façon d’écrire les noms composés : longueurNom, longueur_nom, ou même combiner les 2 : Longueur_Nom. Personnellement, je ne fais aucune préconisation de ce côté. Dans le monde Java et Scala, c’est la 1er choix qui est la norme, comme dans les recommandations d’Oracle : longueurNom, avec les variations sur la casse de l’initiale en fonction du contexte (classe, méthode, …).

Cohérence dans le choix et l’ordre des mots

Il faut rester cohérant aussi dans le choix des mots. Si une variable s’appelle maxLongueur à un endroit, il ne faut pas que la même entité soit nommée maxTaille ou longueurMax ailleurs. C’est vrai aussi pour des concepts similaires. Par exemple, la hauteur et la longueur d’une boîte sont des entités reliées. Il ne faut pas mélanger maxLongueur et hauteurMin. Il est incohérent de mettre max en préfixe, et min en suffixe. De la même manière, si vous utilisez lg comme abréviations de longueur, il faut utiliser une abréviation de même nature pour hauteur. Voici un exemple d’incohérence :

class Caisse {
    int lg;
    int hauteur;
    int largeur;
}

Respecter l’orthographe et la grammaire

Une autre règle qui accroît la cohérance est de respecter l’orthographe et la grammaire. longeur est un très mauvais nom pour une variable. Vous allez vous tromper régulièrement en l’écrivant. De même, lorsque des adjectifs sont collés au nom, il faut respecter le féminin et le pluriel : porteOuverte et portesOuvertes, par exemple. Mais pas l’horrible porteOuvert, sous prétexte que vous avez une autre variable qui s’appelle coffreOuvert.

getter et setter

Le nommage dans le monde Java a subi l’influence des beans. Ce sont eux qui imposent les getters et les setters : getVitesse() et setVitesse(int vitesse). À l’origine, les spécifications pour les beans ont été écrites pour que des outils puissent facilement manipuler les propriétés d’un objet. Hélas, les contraintes ont été imposés à l’humain aussi, même bien après que les outils aient évolués et n’aient plus besoin de noms commençant par get et set.

Je vais donner un exemple à propos du framework Spring. Aujourd’hui, il est possible d’utiliser le framework pour initialiser un objet en utilisant le constructeur. Mais beaucoup de développeurs ont gardé leurs vieilles habitudes, et n’utilisent que des setters pour créer un objet, même si la classe en question doit être immutable.

Je préconise de ne jamais utiliser de getter (une méthode commençant par get). Le setter (une méthode commençant par set) peut être parfois utilisé. Cette recommandation est d’ailleurs celle par défaut dans d’autres langages que Java, comme Scala.

Nom, verbe, adjectif

Voici un exemple, écrit d’abord avec des getters et setters, puis de la manière que je préconise :

if (voiture.getVitesse() > route.getVitesseLimite()) {
  voiture.setVitesse(route.getVitesseLimite());
}

if (voiture.vitesse() > route.vitesseLimite()) {
  voiture.ralentir(route.vitesseLimite());
}

La 2e écriture est beaucoup plus naturelle et facile à lire. Les règles que j’y ai appliquées sont :

  • Un nom, pour les variables et pour les méthodes qui rendent une valeur, comme vitesse. Certains langages ne font pas la différence entre l’accès à une propriété et l’accès à une méthode sans argument qui rend une valeur. Eiffel et Scala sont dans ce cas.
  • Un adjectif peut être accolé au nom, comme vitesseLimite.
  • Un verbe à l’infinitif pour les actions qui vont modifier quelque-chose, comme ralentir.

Il peut arriver qu’on ne veuille pas distinguer les choses, entre accelerer et ralentir par exemple, et qu’on en reste à setVitesse. En français, cela pourrait être modifierVitesse, mais je vous accorde que le mot modifier est bien plus long que set. Simplement, pour les raisons de cohérence vues plus haut, le choix d’utiliser de set ou modifier (ou autre-chose) doit être le même partout.

Il ne faut pas utiliser d’adjectif tout seul. D’autres mots sont des noms, mais ont souvent le sens d’un adjectif. Je prends par exemple le mot reste. Si c’est la place vacante dans un conteneur, c’est un très mauvais choix de nom. Le mot reste, d’après le dictionnaire, c’est ce qui subsiste d’une chose passée (les restes d’un repas). Utiliser ce mot pour indiquer la place vacante dans un conteneur est donc très mauvais, parce qu’en fait, c’est utiliser l’adjectif restant. Et pour utiliser cet adjectif, il faut un nom. Un nom correct pourrait être placesVidesRestantes ou nbPlacesVacantes.

Les abréviations et le vocabulaire métier

Dans tous les projets, il y a un vocabulaire qui provient du domaine, du métier. Ce vocabulaire est à utiliser en priorité pour nommer les choses. Il peut aussi y avoir des abréviations, qu’il faut utiliser aussi. Il faut un tout petit peu de temps pour les apprendre, mais comme de toute façon il faut comprendre à quoi sert le logiciel qu’on fabrique, il est normal d’apprendre ces noms.

Pour reprendre l’exemple de la caisse :

class Caisse {
    int long;
    int haut;
    int larg;
}

Conclusion

Il ne pas hésiter à prendre un peu de temps pour nommer correctement les variables et méthodes de vos programmes. Le code est écrit une seule fois, mais lu de nombreuses fois. Il faut donc être le plus clair possible, et ça ne se fait pas en une seconde. Il m’arrive de renommer tout un ensemble de choses, lorsque le programme évolue, et que je trouve un meilleur nom pour un concept. Et je me répète : le maître mot est la cohérence.

La récursion terminale

La récursion terminale fait partie de la panoplie des outils d’un langage fonctionnel, même si elle peut être implémentée dans n’importe quel langage.

La récursion terminale est la capacité du compilateur à transformer une fonction récursive en une itération, c’est à dire une boucle. Pour que cela soit possible, il faut que l’appel récursif soit la dernière instruction de la méthode à être évaluée.

L’exemple ultra-classique du calcul de la factorielle

Voyons cela sur l’exemple récursif par excellence : le calcul de la factorielle, écrit en Scala :

def factorielle(n: Int): Int = {
  if (n < 2) 1
  else n * factorielle(n - 1)
}
factorielle(5)

La fonction ci-dessus est bien récursive, et visuellement, l’appel récursif est bien à la fin de la fonction. Mais cet appel récursif n’est pas la dernière instruction de la fonction, puisqu’il faut encore effectuer la multiplication une fois qu’on a obtenu la valeur de factorielle(n - 1).

C’est assez facile de transformer cette fonction pour que la récursion soit terminale :

def factorielle(n: Int, resultat: Int): Int = {
  if (n < 2) resultat
  else factorielle(n - 1, resultat * n)
}
factorielle(5, 1)

Le compilateur Scala va transformer cette fonction récursive en une boucle, équivalente au code suivant :

def factorielle(n: Int, resultat: Int) = {
  var valeur = n
  var res = resultat
  while (valeur >= 2) {
    res = res * valeur
    valeur = valeur - 1
  }
  res
}

Détails Scala

Vous allez me dire que ce 2e argument, la valeur de départ qu’il faut mettre à 1, vous gêne. Moi aussi. De plus, il y a d’autres petites choses à faire, et nous allons régler tout ça.

En Scala, on peut annoter une méthode avec @tailrec, ce qui fait que la compilation échoue si la méthode n’est pas récursive terminale. Vous avez ainsi la certitude qu’elle sera optimisée. De plus, une simple méthode d’une classe, pour avoir cette annotation, ne doit pas pouvoir être remplacée en dérivant la classe. C’est pourquoi le compilateur demande à ce que la méthode soit private ou final. En Scala, il est aussi possible de définir une méthode à l’intérieur d’une autre, ce qui règle ce problème. Il faut de plus définir le type du résultat de la méthode. Le moteur d’inférence de type de Scala ne s’en sort pas sinon. Tout ça mis ensemble pour le calcul de la factorielle donne :

def factorielle(n: Int): Int = {
  import scala.annotation.tailrec

  @tailrec
  def fact(n: Int, resultat: Int): Int =
    if (n < 2) resultat
    else fact(n - 1, resultat * n)

  fact(n, 1)
}
factorielle(5)

J’ai aussi mis le import à l’intérieur de la méthode, mais vous pouvez le déplacer en début du fichier.

Intérêts

Il y a plusieurs intérêts à utiliser la récursion terminale lorsque c’est possible. D’abord, cela permet d’écrire du code sans utiliser de variables mutables.

Ensuite, cela permet de comprendre très simplement ce qui est fait, parce que le corps de la méthode ne traite toujours qu’une seule itération. Il n’y a aucun code pour gérer l’ensemble des itérations.

Avec la récursion terminale, on profite des avantages de la récursivité, sans les inconvénients.

Exemple : donner n nombres dont le total donne m

Un dernier exemple : vous devez fournir une liste de n nombres au hasard dont le total donne m. Aucun nombre ne doit être égal à 0, mais plusieurs de ces nombres peuvent être égaux. Voici comment vous pourriez l’écrire :

def listeNombres(combien: Int, total: Int): List[Int] = {
  val random = new scala.util.Random()
  @scala.annotation.tailrec
  def hasard(n: Int, listeTrouve: List[Int]): List[Int] =
    if (n == 0) listeTrouve
    else {
      val i = random.nextInt(total - 2) + 1
      if (listeTrouve.contains(i)) hasard(n, listeTrouve)
      else hasard(n - 1, i +: listeTrouve)
    }

  val intervalles = hasard(combien - 1, List(total)).sorted

  @scala.annotation.tailrec
  def calcDiff(
    precedent: Int, aTraiter: List[Int], resultat: List[Int]): List[Int] =
    if (aTraiter.size == 0) resultat
    else calcDiff(
      aTraiter.head, aTraiter.tail, resultat :+ (aTraiter.head - precedent))

  nombres(0, intervalles, List())
}

listeNombres(4, 100).foreach(println)

On cherche d’abord n-1 nombres différents, compris entre 1 et m-1 inclus, avec la méthode récursive hasard. On trie cette liste et on y ajoute 0 et m, ce qui définit n intervalles dont le total est m. On crée ensuite la liste des nombres demandés avec une 2e méthode récursive calcDiff, qui calcule les différences.

La signature de la classe Enum en Java

Lorsqu’on déclare un énuméré en Java, par exemple

enum Couleur { Rouge, Vert, Bleu }

c’est en fait comme si on déclarait une classe dérivée de la classe Enum, qui est définie dans le JRE. Nous allons nous intéresser à la signature de cette classe Enum, en fait la construire par étape, avec les explications.

1re tentative

class Enum { … }
class Couleur extends Enum { … }

Ça pourrait fonctionner, sauf que la classe Enum permet d’ordonner les éléments de l’énuméré. Pour faire ça, elle implémente l’interface Comparable<E>

2e tentative

class Enum<E> implements Comparable<E> { … }
class Couleur extends Enum<Couleur> { … }

Pour fournir le type E à Comparable, il faut que Enum soit aussi générique. Il est clair que c’est des éléments de type Couleur que l’on veut comparer. C’est pour cette raison que dans l’exemple, on fournit le type Couleur lui même à la classe Enum.

Ça pourrait déjà fonctionner ainsi. Le problème de cette tentative est que rien n’empêche de mettre une autre classe comme type générique à Enum. Avec cette signature, nous pourrions écrire :

class Couleur extends Enum<Boolean> { … }

ce qui poserait des problèmes pour la comparaison de 2 couleurs.

En fait, il faut que le type générique passé à Enum soit un Enum lui-même.

3e tentative

class Enum<E extends Enum> implements Comparable<E> { … }

Nous nous approchons. Le seul souci, c’est que Enum prenant un type en paramètre, il faut le fournir aussi lorsqu’on écrit <E extends Enum>.

4e tentative

class Enum<E extends Enum<F>> implements Comparable<E> { … }

Là, on indique que le type paramétré doit lui-même être un énuméré, mais il est possible que ce soit un autre énuméré, et pas le type que nous sommes en train de créer nous même. En supposant que le type Langue soit un énuméré existant, on pourrait donc écrire :

class Couleur extends Enum<Langue> { … }

Cela évite déjà de mettre énormément de type à la place de Langue, mais ce n’est pas suffisant.

5e tentative

class Enum<E extends Enum<E>> implements Comparable<E> { … }

La seule différence par rapport à la tentative précédente, est que le type F est remplacé par le type E lui-même. Ce qui finalement, semble même plus simple que la tentative précédente.

Nous avons maintenant la signature correcte du type Enum, tel qu’il existe dans l’API Java.

Ce que j’en tire

Lorsqu’on voit pour la 1re fois la signature réelle du type Enum, on se dit qu’elle est compliquée, que nous aurions été incapable de l’écrire. Mais en la décortiquant comme nous venons de le faire, on comprends en fait très bien ce qui se passe.

L’autre remarque, c’est que tous les langages ont des aspects compliqués, même Java. Mais pour le code que nous écrivons tous les jours, nous pouvons ignorer ces aspects qui ne nous concernent que très rarement. Il n’est en général nécessaire de les maîtriser que si on écrit une librairie ou un framework.

Et pour finir sur un clin d’œil, il y a en Java des choses bien plus compliquées encore. Et cela donne raison à la devise du blog : « Tout est toujours plus compliqué qu’on ne croit, même en tenant compte de cette loi. »

L’hypocrisie des « Warnings »

Les messages affichés par un compilateur se classent en général en 2 catégories : les erreurs et les « warnings ». Les erreurs imposent une action immédiate, sinon le code ne se compile pas. Les warnings indiquent un problème accessoire.

Vous connaissez sûrement l’outil Checkstyle, qui vérifie l’adhésion de votre code à un guide de style. Les règles sont nombreuses, et lorsqu’on le met en route sur un code existant, les warnings le sont aussi. Parmi tous ces warnings, il y en sûrement quelques-uns qui signalent de vrais bugs.

Il arrive hélas fréquemment qu’on ignore cette liste de warnings, en se disant qu’on va la traiter plus tard. Ce comportement est très hypocrite, parce qu’on sait bien au fond de soi que lorsque le projet sera livré, on passera à autre chose, et que les warnings vont rester.

Face à un warning, il y a 4 options possibles :

  1. Corriger le code pour que le warning disparaisse. C’est la meilleure action à faire.

  2. Inhiber localement le warning, en l’encadrant de commentaires CHECKSTYLE:OFF et CHECKSTYLE:ON. Il faut toujours ajouter un commentaire texte expliquant la raison pour laquelle la règle n’est pas respectée.

  3. Changer le paramétrage de l’outil (compilateur, IDE, Checkstyle, …), pour que le type de problème mentionné ne le soit plus. C’est à dire, considérer ce cas comme normal dans tout le code. Tous les warnings similaires vont disparaître d’un seul coup du code.

  4. Laisser traîner le warning. Probablement indéfiniment. C’est la moins bonne action qu’il est possible de faire. Cela revient en fait à ne rien faire. C’est le comportement hypocrite que je dénonce,

Je déconseille très fortement la 4e option. Au point de configurer mes paramètres de compilation pour que les warnings soient des erreurs. Cela impose de les corriger tout de suite, ce qui est souvent très facile à faire, en utilisant l’une des 2 premières options.

On peut considérer que les warnings n’empêchent pas le code de s’exécuter correctement, et ne sont donc pas importants. Le sous-entendu, est que la seule caractéristique qui compte, est le fonctionnement du code. C’est faux. La lisibilité du code est tout aussi importante. Un code lisible est plus facile à comprendre, donc plus facile à faire évoluer ou à maintenir.

Covariance, contravariance et invariance

Dans les langages orientés objets, utilisant les types génériques, il peut être utile de comprendre les aspects concernant la variance. Nous allons voir du code Java pour commencer, puis j’expliquerai comment Scala résout le problème que nous allons rencontrer en Java.

Covariance en Java

Voici d’abord la hiérarchie de classes utilisées dans les exemples de cet article :

   - D
 /
A - B - C

A est à la racine. B et D dérivent de A. C dérive de B.

Les génériques sont utilisées par toutes les classes conteneurs : listes, tableaux, ensemble ou map.

B[] tableauB = new B[3];
tableauB[0] = new B();
tableauB[1] = new C();
A[] tableauA = tableauB;

Le code ci-dessus fonctionne sans problème, comme on s’y attends. On peut évidemment ajouter un B à un tableau de B. C étant un sous-type de B, on peut l’ajouter aussi à un tableau de B.

La 4e ligne fonctionne à cause de la covariance. Cela signifie que puisque B est un sous-type de A, alors tableau de B est un sous-type de tableau de A.

Continuons. Puisque D est un sous-type de A, il est possible d’ajouter un D à un tableau de A, tout comme nous avons ajouté un C à un tableau de B :

tableauA[2] = new D();

Tout ça semble bien naturel, et le compilateur Java accepte ce code sans aucun souci. Mais il y a un gros problème. En effet, nous avons créé un tableau de B au départ, auquel nous avons fini par ajouter un objet de type D. Ça ne devrait pas être possible. Java résout ce problème en ajoutant du code pour vérifier la compatibilité des types à chaque fois qu’un objet est ajouté à un tableau. Cette dernière ligne de code génère l’exception ArrayStoreException, au moment de son exécution.

Solutions possibles

Le problème vient du fait que les tableaux sont à la fois mutables et covariant. Régler un seul de ces 2 aspects suffit pour détecter les problèmes au moment de la compilation.

Si le tableau était non mutable, on ne pourrait pas y ajouter de nouveaux éléments et le problème serait résolu.

B[] tableauB = new B[] { new B(), new C() };

Si le tableau est invariant au lieu de covariant, il n’y aurait pas de problème non plus, puisqu’on ne pourrait pas affecter un tableau de B à un tableau de A. Remarquer que le fait que le tableau soit invariant n’empêche pas d’y ajouter des objets ayant un sous-type de B.

La variance en Scala

En Scala, il y a 2 versions de chaque conteneur, l’une est mutable, l’autre ne l’est pas. Il est très rare qu’on utilise les conteneurs mutables, la programmation fonctionnelle incite à n’utiliser que des objets non mutables.

Les conteneurs mutables sont invariants. Lancez la REPL scala, pour tester le code qui suit. Nous allons utiliser les tableaux dans les exemples, mais le principe reste le même pour les autres conteneurs.

class A()
class B() extends A()
class C() extends B()
class D() extends A()
val tableauB = Array(new B(), new C())

Si nous tentons maintenant de changer le type de ce tableau, nous allons obtenir une erreur de compilation :

val tableauA: Array[A] = tableauB
<console>:11: error: type mismatch;
 found   : Array[B]
 required: Array[A]
Note: B <: A, but class Array is invariant in type T.
You may wish to investigate a wildcard type such as `_ <: A`. (SLS 3.2.10)
       val tableauA: Array[A] = tableauB
                                ^

Il est quand même possible d’ajouter un élément à un conteneur immutable. La raison simple est qu’une nouvelle collection est créée, incluant ce nouvel élément.

val tableau2 = tableauB :+ new B()
tableau2: Array[B] = Array(B@e3a7be4, C@51b12677, B@219bbd08)

Contravariance

Mais là où cela devient intéressant, c’est que l’argument de la méthode d’ajout d’un élément (:+) est contravariant. Cela signifie qu’il est possible d’ajouter un objet d’un type qui est un sur-type de B (A dans notre exemple) :

val tableau3 = tableauB :+ new A()
tableau3: Array[A] = Array(B@e3a7be4, C@51b12677, A@55bce7e2)

Comment est-ce possible ? Tout simplement parce que la nouvelle collection créée a changé de type. Ce n’est plus un Array[B] mais un Array[A]. Et comme A est un sur-type de B, cette nouvelle collection peut bien sûr contenir des objets de type B (ou de ses sous-types).

En fait, on peut même ajouter n’importe quel type à une collection, le résultat étant une collection du sur-type commun à tous les objets de la nouvelle collection. Un tel type existe toujours, puisque en Scala, la racine de la hiérarchie est Any (même pour les types primitifs).

class E()
val tableau4 = tableauB :+ new E()
tableau4: Array[Object] = Array(B@e3a7be4, C@51b12677, E@7430bc45)

val tableau5 = tableauB :+ 1
tableau5: Array[Any] = Array(B@e3a7be4, C@51b12677, 1)

Ce n’est pas du tout recommandé, parce qu’on ne peut pas faire grand chose avec une collection de Object ou de Any.

Conclusion

Utiliser des objets immutables a bien des avantages, n’est-ce-pas ? Il existe des règles claires pour les endroits où il faut utiliser la covariance, la contravariance ou l’invariance. Mais je ne vais pas entrer dans ces détails, et vous n’avez pas besoin de les connaître. Sachez qu’en Scala, les collections respectent ces règles et ne vous poserons pas de problème.

Les nombres rationnels : premier code Scala

3 niveaux de maîtrise de Scala

Martin Odersky, le créateur de Scala, a définit 3 niveaux de maîtrise du langage. Le 1er niveau peut être vu simplement comme un « meilleur Java », et les idées sont très facile à maîtriser pour un développeur Java. Ci-dessous est créée une classe qui montre quelques concepts simples pour introduire le langage.

REPL

Scala dispose d’une REPL. Cela signifie Read-Eval-Print-Loop, soit en français Lire-Évaluer-Écrire-Recommencer. C’est un outil en ligne de commande qui permet d’exécuter directement du code Scala, sans avoir à passer par la phase de compilation. Cet outil se lance avec la commande scala sans argument. Le code qui suit peut être directement copié dans la REPL.

Les nombres rationnels

Les nombres rationnels sont ceux qui s’écrivent comme un quotient de 2 entiers relatifs, noté a/b. a est nommé le numérateur et b le dénominateur. Par exemple : 1/2, -3/5, 1234/3, ou encore 10/1.

class Rationnel(val numerateur: Int, val denominateur: Int)

Cette unique ligne suffit pour définir la classe, avec les 2 champs, qui sont publics et non modifiables. On peut maintenant écrire

val r = new Rationnel(1, 2)
println("Numérateur = " + r.numerateur)

val définit une valeur immutable, aussi bien dans le constructeur de la classe que lors de la création du rationnel r. Le constructeur par défaut est celui avec les arguments juste après le nom de la classe. Il n’y a pas de point-virgule (;), qui seraient nécessaire en Java, mais sont optionnels en Scala.

Le type de r est déterminé automatiquement par le compilateur, en fonction du type de la partie droite. Cette détection du type par le compilateur est très puissante, et permet de réduire énormément le code à saisir. On aurait pu écrire

val r : Rationnel = new Rationnel(1234, 3)

Le mot clé val définit une valeur immutable. Écrire ensuite r = new Rationnel(10, 1) génère une erreur de compilation. Le mot clé var permet de définir une variable que l’on peut réassigner.

Version améliorée de la classe

Voici une 2e version de la classe.

case class Rationnel(numerateur: Int, denominateur: Int) {
  def this(n: Int) = this(n, 1)
  val reel = numerateur.toFloat / denominateur
  println("valeur réelle = " + reel)
  override def toString = numerateur.toString + " / " + denominateur
}
val negatif = Rationnel(-3, 5)
val dix = new Rationnel(10)
println(dix.toString)

Un constructeur auxiliaire a été définit. Les constructeurs auxiliaires doivent toujours référencer un autre constructeur, et en bout de chaîne il doit y avoir le constructeur par défaut. Le corps du constructeur est le contenu de la classe lui-même. Ici est définit puis affichée la valeur reel, dans le constructeur.

Le mot def définit une fonction ou une méthode.

toString surcharge la méthode sans argument bien connue des classes Java. Les parenthèses pour l’appel de la méthode sont optionnelles. Du coup, lors de son utilisation, il n’est plus possible de faire la différence entre l’accès à un membre, et l’appel d’une méthode sans paramètre. Cette idée se base sur le fait que dans la programmation fonctionnelle, on cherche à n’utiliser que des objets immutables. Donc les méthodes renvoient toujours la même valeur avec les mêmes paramètres d’entrée. Savoir si on utilise une méthode ou directement un membre n’a donc plus d’importance.

Le plus remarquable ici, est l’ajout du mot clé case. Cela définit ce qu’on appelle de manière très originale une case class. Elle a de nombreuses propriétés intéressantes :
– les méthodes equals, hashCode et toString sont définies par défaut, en utilisant les valeurs du constructeurs par défaut.
– lors de la création d’un objet avec le constructeur par défaut, le mot clé new n’est plus nécessaire, grâce à l’utilisation de la fonction apply dans l’objet compagnon (voir ci-dessous).
– les arguments du constructeur sont des val par défaut. Ce mot clé est optionnel.

Objet compagnon

L’objet compagnon d’une classe sert à définir toutes les méthodes static en Java.

Voici comment ajouter une méthode dans l’objet compagnon, pour simplifier aussi la création des entiers, ainsi que la création d’une constante.

case class Rationnel(val numerateur: Int, val denominateur: Int) {
  def this(n: Int) = this(n, 1)
}
object Rationnel {
  def apply(n: Int) = new Rationnel(n, 1)
  val un = Rationnel(1)
}
println(Rationnel.un.toString)

Les méthodes apply qui sont définies dans l’objet compagnon sont particulières, parce qu’elles peuvent être appelées en utilisant simplement le nom de la classe. Ainsi ces 2 lignes sont équivalentes :

val moinsUn = Rationnel(-1)
val moinsUn = Rationnel.apply(-1)

Conclusion

Voilà la présentation de quelques concepts simples, qui font de Scala un langage compact et très lisible, sans même utiliser l’aspect fonctionnel du langage. La classe Rationnel ci-dessus est bien sûr entièrement compatible avec Java.

Scala, c’est quoi ?

Présentation

Définir Scala se fait par rapport à Java.

Scala est un langage qui se compile pour la JVM. Il a un typage statique et intègre 2 paradigmes : la programmation orientée objet, et la programmation fonctionnelle. Le langage a été conçu à l’École polytechnique fédérale de Lausanne (EPFL) par Martin Odersky, qui a conçu les génériques pour Java, et a écrit la base du compilateur Java actuel.

Le langage date de 2001. La première version publique date de 2003. Aujourd’hui, il est utilisé par des entreprises mondiales, par exemple Twitter ou The Guardian. Martin Odersky travaille pour la société Typesafe, dont le but est de développer, promouvoir et offrir des services autour de Scala. Le langage a quitté le monde académique pour entrer dans le monde industriel il y a quelques années déjà.

Compatibilité Scala / Java

Les classes Java sont utilisables en Scala, et réciproquement. Cela signifie qu’en Scala, il est possible d’utiliser les nombreuses librairies Java existantes, y compris la librairie standard Java.

Il est aussi possible d’utiliser en Java une librairie écrite en Scala. Un excellent exemple est Akka, qui implémente le modèle d’acteur, pour la programmation concurrente.

Une fois le code Scala compilé, on obtient des fichiers jar, qui se comportent comme n’importe quelle librairie Java. Il n’y a plus aucune différence, du point de vue de la JVM. Il y a évidemment la librairie standard Scala, qui s’ajoute nécessairement à votre projet.

Programmation fonctionnelle

Le grand intérêt de Scala est l’introduction de la programmation fonctionnelle. Avec les autres améliorations syntaxiques par rapport à Java, cela permet d’écrire beaucoup moins de code, pour exprimer les mêmes besoins, tout en restant très lisible. Certes, il faut apprendre les bases de la programmation fonctionnelle, mais cela vient vite — ce n’est pas plus compliqué que d’apprendre à utiliser une nouvelle librairie Java.

Les outils

Il y a quelques années encore, un gros reproche que l’on faisait à Scala est le manque d’outils de développement par rapport à Java. Il est clair que ces outils sont très nombreux pour Java, et que l’attente des développeurs est hautes sur ce sujet. Cela a été la priorité de la société Typesafe, qui a amélioré le plugin Scala de Eclipse. Il a aujourd’hui un très bon niveau. Il existe aussi un excellent plugin pour IntelliJ IDEA. Personnellement, j’utilise ensime pour Emacs.

Introduction dans un projet existant

Une idée pour introduire doucement Scala dans un projet existant, est de le cantonner à l’écriture des tests JUnit. On peut ainsi apprivoiser le langage, sans impact sur le code en production. Mais attention, une fois qu’on y a goûté, c’est très difficile de revenir en arrière.

Java, c’est quoi ?

Java, c’est 3 choses distinctes, qui semblent former un tout. Mais chacune des 3 parties peut être remplacée, avec plus ou moins de bonheur. Voyons les en détails.

Java est une machine virtuelle

La machine virtuelle Java (JVM) est un bijou de technologie. C’est un logiciel très complexe, qui fait des merveilles. S’il n’y avait qu’une seule chose à garder du monde Java, c’est bien la JVM.

L’un des grands avantages non technique de la JVM, est quelle a été portée sur de nombreux environnement, et qu’il est donc possible, même s’il faut faire attention aux détails, d’écrire du code une seul fois qui fonctionne partout.

Remplacer la JVM n’a pas grand sens, vu sa qualité, mais il existe des alternatives. Excelsior JET est un produit payant, qui compile le code Java pour donner un executable natif. Il est compatible Linux, Mac et Windows. IKVM.NET est une implémentation de Java pour Mono et le framework .NET.

Je n’ai jamais utilisé aucun des 2 produits nommés ci-dessus, et ne peux en aucun cas vous parler de leur qualité ou défaut. Je les signale juste pour bien montrer que les alternatives sont réelles.

Java est un langage

C’est l’aspect auquel tout le monde pense lorsqu’on parle de Java : le langage. Les 2 autres aspects sont souvent implicites. Dans ce domaine, il existe de nombreux autres langages qui peuvent être compilés pour donner des classes Java, qui fonctionnent ensuite sur la JVM.

La liste est longue, entre ceux qui existent depuis longtemps (Groovy, Jython, JRuby et Scala par exemple), et ceux qui ont été créés plus récemment (Closure, Kotlin). Voici un article en anglais de Dr.Dobb’s, datant de novembre 2012, qui en présente quelques-uns rapidement.

Personnellement, le seul que je connais vraiment, parce que je l’utilise quotidiennement depuis 2 ans, est Scala.

Une chose importante pour un tel langage, est la manière dont il s’intègre dans l’écosystème existant, donc s’il est possible de réutiliser les nombreuses librairies existantes écrites en Java.

Java est un ensemble de librairies

L’ensemble des fonctionnalités couvertes par les librairies Java, aussi bien celle venant avec la JVM, que toutes celles que l’on peut trouver sur le net, est immense. Il y a même souvent plusieurs choix possibles pour une fonctionnalité. Au niveau techniques, vous voulez un conteneur pour l’injection de dépendances ? Spring, Guice, ou PicoContainer. Les classes par défaut pour la gestion des dates et du temps ne vous plaisent pas ? [JodaTime] http://www.joda.org/joda-time/). Au niveau des fonctionnalités, nous ne sommes pas en reste. Que vous ayez besoin de serialiser en json ou yaml, de logguer (SLF4J et Logback), d’un ORM, ou de faire du calcul numérique. Cette toute petite liste est très loin d’être exhaustive.

Choix d’une police de caractères pour la programmation

Lorsque vous utilisez un IDE pour développer, la fonte(1) par défaut n’est pas forcément la meilleure. Il y a quelques critères qui sont importants.

Tout d’abord, il faut prendre conscience qu’un programme n’est pas un article de journal. Dans du code source, il est parfois bien plus difficile de faire la différence entre un l et un 1 en fonction du contexte. Il faut donc que la fonte permettent de distinguer clairement entre les caractères i, l et 1 (la voyelle i, la lettre L minuscule, le chiffre un). Idem pour o, O et 0 (la voyelle O minuscule, la voyelle O majuscule, le chiffre zéro).

Avec les écrans LCD récents, nous avons en général bien plus d’espace horizontal que d’espace vertical. Or il important d’avoir un maximum de lignes de programme sur l’écran. Il faut donc privilégier une petite fonte.

L’alignement du code étant important, il faut bien sûr utiliser une fonte de taille fixe où tous les caractères, même l’espace, ont la même largeur.

Le dernier critère est subjectif. Comme vous passer de nombreuses à lire du code, il faut que vous trouviez que la fonte que vous utilisez soit claire, lisible, et qu’elle vous plaise.

J’ai choisi la fonte Proggy. C’est une fonte bitmap, mais elle existe aussi en version TTF. Il y a plusieurs variantes. J’utilise Proggy Clean après en avoir essayé 2 ou 3. Certains préfèrent la fonte Dina, qui est basé sur Proggy. (Pour Linux, qui ne permet pas d’installer une fonte .fon, il existe une version TTF de Dina).

Exemple de font Proggy clean

Sur mon écran 19 pouces, j’affiche 57 lignes dans Eclipse avec Proggy, contre 47 avec la fonte par défaut.

Le seul reproche que je fais à cette fonte, est que sa largeur n’est pas la même en normal et en gras. Lorsque Eclispe met du code en couleur, il utilise du gras pour les mots réservés. Cela ne me gêne que pour ne pas avoir des lignes ayant plus de 80 caractères. Il n’y a aucun problème avec l’indentation du code.

Il existe de nombreux autres articles à ce propos, la plupart en anglais. En voici un en français, et l’autre en anglais :
Polices de caractères pour programmeur
Consolas and ClearType by Jeff Atwood

(1) Comme l’indique le titre, c’est bien du choix d’une police de
caractères dont il s’agit. Mais pour ne pas trop rallonger le texte, je vais utiliser le terme fonte, qui ne désigne pas tout à fait la même chose. Une fonte désigne l’ensemble des caractères correspondant aux mêmes caractéristiques de corps, graisse et italique au sein d’une même police. Vous pouvez en apprendre plus sur les polices d’écriture sur Wikipédia.

Popeye et les kangourous

Comme l'indique le diagramme ci-contre, le sujet d'aujourd'hui est un problème d'échecs.

Depuis le 13 novembre est lancé sur le site caissa-france.net le concours du pat aidé de série le plus long aboutissant au diagramme ci-contre.

La pièce inhabituelle en ç5 et é5 est un kangourou. Cette pièce saute au-dessus de 2 autres pièces et atterrit juste après la 2e. Les détails sont accessibles depuis la page donnant le règlement du concours.

Popeye est un logiciel gratuit qui résout toutes sortes de problèmes d'échecs, notamment les pat aidés de série avec des kangourous au diagramme.

Pour m'aider dans ma recherche, j'ai écrit un programme, en shell, qui soumet à Popeye toutes les positions possibles pour le roi et les kangourous noirs, en imposant un dernier coup blanc. Ci-dessous se trouve le listing de ce programme. Dans cet exemple, le coup blanc qui fait pat est Ré2-d2.

Il est évident qu'une telle recherche est très longue. Je n'ai pas eu le temps de faire des recherches pour 6 pièces avec tous les derniers coups blancs possibles. La recherche avec 7 pièces au diagramme, en ajoutant par exemple un kangourou noir, est encore énormément plus longue.

L'intérêt de ce programme est de voir les positions intermédiaires trouvées, et d'en tirer un sentiment sur les idées qui marchent et celles qui ne marchent pas. Pour atteindre le record, il faut encore un gros travail sur l'échiquier.

#!/bin/sh

casesRoi="a1 a2 a3 a4 a5 a6 a7 a8 b1 b2 b3 b4 b5 b6 b7 b8 c1 c2 c3 c5 c6 c7 c8 d4 d6 d7 d8 e5 e6 e7 e8 f4 f6 f7 f8 g1 g2 g3 g4 g5 g6 g7 g8 h1 h2 h3 h4 h5 h6 h7 h8"

casesKangourou="a1 a2 a3 a4 a5 a6 a7 a8 b1 b2 b3 b4 b5 b6 b7 b8 c1 c2 c3 c4 c5 c6 c7 c8 d1 d2 d4 d5 d6 d7 d8 e1 e3 e5 e6 e7 e8 f1 f2 f3 f4 f5 f6 f7 f8 g1 g2 g3 g4 g5 g6 g7 g8 h1 h2 h3 h4 h5 h6 h7 h8"

FOUND=0
COOKED=1
NO_SOL=2

choixRoi()
{
    ka1=$1
    ka2=$2

    for roi in $casesRoi
    do
	if test $roi = $ka1 -o $roi = $ka2
	then
	    :
	else
	    dichotomie $roi $ka1 $ka2
	fi
    done
}

dichotomie()
{
    roi=$1
    ka1=$2
    ka2=$3

    min=2
    max=100

    resoudre $roi $ka1 $ka2 $min
    resMin=$?
    if test $resMin -ne $NO_SOL
    then
	return
    fi

    resoudre $roi $ka1 $ka2 $max
    resMax=$?
    if test $resMax -eq $NO_SOL
    then
	echo $ka1$ka2${roi}n0 >> noSolution
	return
    elif test $resMax -eq $FOUND
    then
	return
    fi

    (( delta= $max - $min ))
    while test $delta -gt 1
    do
	(( milieu=($max + $min) / 2 ))
	resoudre $roi $ka1 $ka2 $milieu
	res=$?
	case $res in
	    0)
		return
		;;
	    1)
		max=$milieu
		resMax=$res
		;;
	    2)
		min=$milieu
		;;
	esac
	(( delta= $max - $min ))
    done
}

resoudre()
{
    roi=$1
    ka1=$2
    ka2=$3
    n=$4

    file=$roi.txt
    cat >$file < result.txt 2>/dev/null
    ok=`grep -c "e2-d2" result.txt`
    set `wc -l result.txt`
    nbLignes=$1
    if test ${nbLignes} -ge 30
    then
	result=$COOKED
    elif test ${nbLignes} -eq 28
    then
	result=$NO_SOL
    elif test ${nbLignes} -eq 29
    then
	if test $ok -eq 1
	then
	    echo -n "+"
	    dir=n${n}
	    if test ! -d $dir
	    then
		mkdir $dir
	    fi
	    res=$dir/$ka1$ka2${roi}n${n}.txt
	    cp $file $res
	    cat result.txt >> $res
	fi
	result=$FOUND
    else
	res=etrange$ka1$ka2${roi}n0.txt
	cp $file $res
	cat result.txt >> $res
	result=$NO_SOL
    fi
    rm $file
    rm result.txt
    return $result
}

for ka1 in $casesKangourou
do
    echo ""
    echo -n "Position $ka1 :"
    for ka2 in $casesKangourou
    do
	if [[ $ka2 > $ka1 ]]
	then
	    echo -n " $ka2"
	    choixRoi $ka1 $ka2
	fi
    done
done