Images continues II

CS-108 — Corrigé de la série 5

Introduction

Comme d'habitude, le code du corrigé est disponible dans une archive Zip, et les solutions des différents exercices sont rapidement décrites ci-dessous.

Exercice 1

La principale difficulté de cet exercice consiste à comprendre comment transformer en méthodes statiques les classes dont le constructeur prend des arguments.

Comme expliqué dans l'énoncé, l'idée est que la méthode statique correspondant à une classe doit prendre les mêmes arguments que le constructeur de la classe. Cela est logique, puisque ces arguments constituent les paramètres de l'image à produire.

L'image elle-même est bien entendu construite au moyen d'une lambda, puisqu'une image a le type Image, qui est une interface fonctionnelle. Cette lambda définit la seule méthode abstraite de Image, à savoir apply, et son code est donc identique au code de la méthode apply de la classe.

Par exemple, la classe Chessboard peut être transformée en la méthode statique chessboard ci-dessous :

static Image<ColorRGB> chessboard(ColorRGB c1,
                                  ColorRGB c2,
                                  double w) {
  if (! (w > 0))
    throw new IllegalArgumentException();

  return (x, y) -> {
    int sqX = (int)floor(x / w), sqY = (int)floor(y / w);
    return (sqX + sqY) % 2 == 0 ? c1 : c2;
  };
}

Le principe est identique pour les autres classes dont le constructeur prend des arguments. Celui de la classe HorizontalGradientMask n'en prenant pas, on la traduit de la même manière que RedDisk.

Finalement, la méthode rotated peut, comme expliqué dans l'énoncé, être transformée en méthode par défaut. Pour cela, il suffit de :

  • lui ajouter le modificateur default,
  • supprimer l'argument image qu'elle prenait initialement, celui-ci étant désormais (implicitement) this, et
  • remplacer l'appel à image.apply par un appel à this.apply.

On obtient alors :

default Image<T> rotated(double angle) {
  double cosA = cos(-angle);
  double sinA = sin(-angle);
  return (x, y) -> {
    double x1 = x * cosA - y * sinA;
    double y1 = x * sinA + y * cosA;
    return apply(x1, y1);
  };
}

Une fois la traduction de la totalité des images terminées, on peut constater que la réduction du nombre de lignes obtenue grâce aux lambdas est impressionannte puisque l'interface Image contient presque la moitié moins de lignes que l'ancienne version du code.

Exercice 2

Pour faciliter la compréhension du code, nous avons choisi de définir une classe enregistrement minimaliste pour les nombres complexes, nommée Complex et disponible dans l'archive référencée plus haut. Elle n'est pas présentée ici en détail, mais elle offre simplement trois méthodes :

  • double squaredModulus(), qui retourne le module du nombre élevé au carré (ce qui suffit ici, et évite le calcul d'une racine carrée),
  • Complex squared(), qui retourne le récepteur élevé au carré,
  • Complex add(Complex that), qui retourne la somme du récepteur this et de l'argument that.

Grâce à cette classe, le corps de la lambda retournée par la méthode mandelbrot est une traduction assez directe de la spécification donnée dans l'énoncé. Le terme courant de la suite est contenu dans la variable z, tandis que son index est contenu dans la variable i.

static Image<Double> mandelbrot(int maxIt) {
  if (! (maxIt > 0))
    throw new IllegalArgumentException();

  return (x, y) -> {
    Complex c = new Complex(x, y);
    Complex z = c;
    int i = 1;
    while (i < maxIt && z.squaredModulus() <= 4d) {
      z = z.squared().add(c);
      i += 1;
    }
    return (double)i / (double)maxIt;
  };
}

Il faut noter que le corps de la lambda peut également s'écrire au moyen de la programmation par flot :

return (x, y) -> {
  Complex c = new Complex(x, y);
  double i = Stream.iterate(c,
                            z -> z.squaredModulus() <= 4d,
                            z -> z.squared().add(c))
    .limit(maxIt)
    .count();
  return i / maxIt;
};

Finalement, la définition de la méthode constant ne pose pas de gros problème :

static <T> Image<T> constant(T v) {
  return (x, y) -> v;
}

Il faut noter que, comme il s'agit d'une méthode générique, elle peut aussi bien être utilisée pour obtenir une image colorée constante qu'un masque constant.