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

Passer de Java à Scala – Partie 4 – Les objets fonctionnels

Passer de Java à Scala – Partie 4 – Les objets fonctionnels

Dans cette 4è partie, nous allons parler des objets fonctionnels en Scala. Mais avant toute chose, un mot sur la programmation fonctionnelle.

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

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

Programmation fonctionnelle

Le premier langage de programmation fonctionnel fut Lisp. D’autres langages fonctionnels ont acquis une certaine popularité comme Haskell, Erlang ou OCaml.

La programmation fonctionnelle est fondée sur 2 idées : la première est que les fonctions sont des valeurs de « première classe » au même titre que les objets ou les types primitifs. Cela signifie que des fonctions peuvent par exemple être passées en paramètres d’autres fonctions.

Par contraste, les langages plus traditionnels considèrent les fonctions comme des valeurs de « seconde classe ». Les pointeurs de fonctions en C ou C++ n’apportent pas la même flexibilité que si elles étaient considérées comme des valeurs de première classe (un pointeur de fonction en C référence une fonction globale par exemple).

La 2ème idée de la programmation fonctionnelle est que les opérations d’un programme doivent prendre une entrée et renvoyer une sortie sans modifier les données durant cette opération. Par exemple,  une fonction doit prendre une valeur en paramètre et retourner une autre valeur en sortie, sans modifier la valeur passée en paramètre. On dit encore que les fonctions ne doivent pas créer d’effet de bord. Les structures de données immuables sont la pierre angulaire de la programmation fonctionnelle.

Scala étant un hybride entre programmation objet et fonctionnelle, on tend à utiliser les 2 paradigmes pour programmer en Scala. Les objets fonctionnels représentent cette mixité des paradigmes.

Création d’un objet pour les fractions mathématiques

Dans cet article, nous allons créer un objet fonctionnel qui prend 2 paramètres : un numérateur et un dénominateur. Cet objet s’affichera sous la forme 3/4 et permettra d’effectuer des opérations telles que 3/4 + 5/2 , 1 + 5/8 etc…

Nous avons vu dans l’article précédent comment créer une classe ainsi qu’un constructeur auxiliaire et des getters pour les attributs de la classe.

Définition de la classe

Nous allons repartir de ces principes pour créer la classe fraction qui prendra en paramètres un numérateur et un dénominateur :

class Fraction(n: Int, d: Int) {
  val num = n
  val den = d
}

Si l’on créé un nouvel objet Fraction dans la console Scala, on voit s’afficher

scala> new Fraction(5, 2)
res0: Fraction = Fraction@7d9c45ee

Surcharge de fonctions

On voit que l’objet retourné est affiché sous la forme Fraction@<id_en_memoire>, ce qui n’est pas très lisible. On va donc redéfinir la méthode toString pour obtenir un affichage plus parlant :

class Fraction(n: Int, d: Int) {
  val num = n
  val den = d

  override def toString: String = num + "/" + den
}

La surcharge de fonctions définies dans les classes parentes se fait via le mot-clé override. Maintenant, si on créé un objet fraction on obtient :

scala> new Fraction(5, 2)
res0: Fraction = 5/2

Pré-conditions

Par définition, une fraction ne peut pas prendre la valeur 0 pour dénominateur (division par 0 = l’univers explose). On peut ajouter une précondition au constructeur de la classe avec le mot-clé require :

class Fraction(n: Int, d: Int) {
  require(d != 0)

  val num = n
  val den = d

  override def toString: String = num + "/" + den
}

Ainsi, si on essaye de passer 0 au dénominateur, on obtiendra une IllegalArgumentException :

scala> new Fraction(5, 0)
java.lang.IllegalArgumentException: requirement failed
  at scala.Predef$.require(Predef.scala:264)
  ... 30 elided

On notera que la fonction require appartient au package Predef importé par défaut dans tout fichier Scala.

Constructeur auxiliaire

Il serait intéressant de pouvoir réaliser des opérations comme 5/2 + 2 sans avoir à écrire explicitement 5/2 + 2/1. On va donc ajouter un constructeur auxiliaire qui ne prendra en argument qu’un numérateur, le dénominateur sera initiliasé à 1 :

class Fraction(n: Int, d: Int) {
  require(d != 0)

  val num = n
  val den = d

  def this(n: Int) = this(n, 1)
  override def toString: String = num + "/" + den
}

On peut désormais écrire :

scala> new Fraction(2)
res2: Fraction = 2/1

Réduction de fractions

Imaginons maintenant que l’on créé la fraction 4/8. On voit que cette fraction peut-être réduite à 1/2. Faisons en sorte que la classe Fraction calcule le plus grand dénominateur commun et initialise ses champs num et den aux valeurs réduites :

class Fraction(n: Int, d: Int) {
  require(d != 0)

  private val g = gcd(n.abs, d.abs)

  val num = n / g
  val den = d / g

  def this(n: Int) = this(n, 1)
  override def toString: String = num + "/" + den

  private def gcd(a: Int, b: Int): Int = {
    if(b == 0) a else gcd(b, a % b)
  }
}

On programme une fonction privée gcd qui calcule le plus grand dénominateur commun g. On passe à cette fonction les valeurs absolues des numérateurs et dénominateurs qu’ensuite on divise par le dénominateur commun g.

Si on écrit une fraction 4/8, celle-ci sera automatiquement réduite :

scala> new Fraction(4, 8)
res3: Fraction = 1/2

Addition de fractions

Créons maintenant une méthode add qui prend une Fraction en paramètre et retourne une nouvelle Fraction :

class Fraction(n: Int, d: Int) {
  require(d != 0)

  private val g = gcd(n.abs, d.abs)

  val num = n / g
  val den = d / g

  def this(n: Int) = this(n, 1)
  override def toString: String = num + "/" + den

  def add(that: Fraction): Fraction =
    new Fraction(num * that.den + that.num * den, den * that.den)

  private def gcd(a: Int, b: Int): Int = {
    if(b == 0) a else gcd(b, a % b)
  }
}

En exécutant ce code, on obtient :

scala> new Fraction(1, 8)
res0: Fraction = 1/8

scala> res0.add(new Fraction(2, 8))
res1: Fraction = 3/8

Définition d’opérateurs

En Scala, il est possible de définir des fonctions ayant pour nom des opérateurs unaires comme +, -, *, / etc… Comme indiqué dans mon premier article sur le passage de Java à Scala, lorsque l’on écrit 1 + 2, c’est en réalité la méthode +() qui est appelée sur l’objet 1 et qui prend en paramètre 2. On peut tout aussi bien écrire (1).+(2).

Le signe + est donc un nom valide pour une fonction :

class Fraction(n: Int, d: Int) {
  require(d != 0)

  private val g = gcd(n.abs, d.abs)

  val num = n / g
  val den = d / g

  def this(n: Int) = this(n, 1)
  override def toString: String = num + "/" + den

  def add(that: Fraction): Fraction =
    new Fraction(num * that.den + that.num * den, den * that.den)

  def +(that: Fraction) = add(that)

  private def gcd(a: Int, b: Int): Int = {
    if(b == 0) a else gcd(b, a % b)
  }
}

On peut donc maintenant écrire :

scala> new Fraction(1, 8)
res0: Fraction = 1/8

scala> res0 + new Fraction(2, 8)
res1: Fraction = 3/8

Attention à la redéfinition d’opérateurs

Bien que la surcharge d’opérateurs soit très utile pour la lisibilité (les programmeurs en C++ en sauront quelque chose), il faut bien faire attention à ne pas en abuser. En effet, rien n’empêche d’écrire

def >(that: Fraction) = add(that)

Et ainsi avoir l’opérateur > effectuer une addition, ce qui est contre-intuitif

scala> new Fraction(2, 8)
res0: Fraction = 1/4

scala> res0 > new Fraction(3, 8)
res1: Fraction = 5/8

Soyez donc prudents avec la définition d’opérateurs afin que le code soit toujours compréhensible et simple à lire.

Conversion implicite

Pour clore cet article, voyons comment nous pouvons utiliser la conversion implicite pour pouvoir écrire des opérations sur les Fractions sans avoir à créer d’instances de l’objet.

Par exemple, si l’on essaye d’écrire :

scala> val r = new Fraction(2, 3)
r: Fraction = 2/3

scala> 2 + r

On obtiendra l’erreur suivante :

<console>:13: error: overloaded method value + with alternatives:
  (x: Double)Double <and>
  (x: Float)Float <and>
  (x: Long)Long <and>
  (x: Int)Int <and>
  (x: Char)Int <and>
  (x: Short)Int <and>
  (x: Byte)Int
 cannot be applied to (Fraction)
       2 + r

Pour permettre cette opération, il faut créer une conversion implicite qui est capable de transformer automatiquement un entier en fraction :

scala> implicit def intToFraction(x: Int) = new Fraction(x)

On obtiendra alors :

scala> val r = new Fraction(2, 3)
r: Fraction = 2/3

scala> 3 + r
res0: Fraction = 11/3

A noter que la conversion implicite doit se trouver dans le scope global de l’application. Si elle se trouve au sein de la classe, elle ne sera visible que par la classe elle-même.

Conclusion

Voilà pour ce chapitre sur les objets fonctionnels en Scala. Dans la prochaine partie, nous détaillerons les structures de contrôles de Scala.

 

Laisser un commentaire