Le polymorphisme, qui permet à une opération d'être appelée sur des types différents, est l'une des bases de la programmation orientée objet. Et pourtant elle ne lui est pas exclusivement réservée. C'est en effet un terme que nous retrouvons en programmation fonctionnelle à travers la notion de typeclasse.

La notion de typeclasse n'a en effet rien à voir avec les classes et les interfaces de la programmation orientée objet. Mais elle présente certains point communs. Bon, déjà, il y a classe dedans... Mais ça c'était facile !

En fait, les typeclasses permettent de "catégoriser" des types divers et de leur associer une interface ou une API commune. Sauf que dans le cas des typeclasses, les possibilités d'extension et de mise en application sont beaucoup plus étendues que dans le cadre des classes, jusqu'à permettre de mieux s'en sortir dans un cadre legacy aussi fermé soit-il. Ceci est facilité parce que les typeclasses se basent plus sur la délégation que sur l'héritage.

Du classique à l'héritage

Prenons un exemple : j'ai des chiens et des dinosaures représentés par les types Dog et Dinosaur. Je devrais pouvoir utiliser la même opération walk pour voir les chiens et les dinosaures marcher, pour laquelle seul va changer le comportement associé.

En programmation plus ou moins classique, on partirait sur une approche utilisant la réflexion pour connaître le type de l'instance et une structure conditionnelle pour y associer un traitement spécifique.

public String walk(Object animal) {
  if (animal.isInstanceOf[Dog])
    return "Tap tap tap tap tap tap tap tap";
  else if (animal.isInstanceOf[Dinosaur])
    return "Boom boom acka-lacka lacka boom";
  else
    throw new Exception("your animal doesn't walk. HAhaHAha!");
}

(Depuis les années 80, on sait que les dinosaures font ce bruit lorsqu'ils se déplacent !)

Le problème de cette approche est que le développeur doit penser au cas d'erreur. En cas d'oubli, seule l'exécution du programme avec la bonne donnée en entrée permet de rappeler que la gestion du cas exceptionnel est manquant. C'est une situation qui peut apparaître OÙ ELLE VEUT ET C'EST SOUVENT DANS LA PROD...

https://s3-us-west-2.amazonaws.com/secure.notion-static.com/f5c73233-0cdc-41d6-90bb-dd4927a586b3/Untitled.png

Il y a d'autres aspects qui sont relativement problématiques ici. Par exemple, ajouter un animal comme le canard, mais oublier de l'ajouter dans walk. Ce qui va se traduire par une exception. De plus, si ce code vient du bibliothèque tierce scellée (ie. les modifications dans cette bibliothèque sont... c'est très compliqué), ajouter le comportement pour le canard... Et bien... on peut pas 😕. Par contre, si je veux ajouter une opération dance pour mes animaux, ce ne sera pas un soucis.

La programmation orientée objet apporte une solution à ce problème en ne permettant pas au développeur de faire n'importe quoi, sauf lorsqu'il l'a clairement exprimé 🤔

trait Animal { /* ... */ }

trait Walking {
  def walk: String
}

case class Dog(name: String) extends Animal with Walking {
  /* ... */
  def walk: String = "Tap tap tap tap tap tap tap tap"
}
case class Dinosaur(name: String) extends Animal with Walking {
  /* ... */
  def walk: String = "Boom boom acka-lacka lacka boom"
}

def walk(walking: Walking): String = walking.walk

Si je veux ajouter le canard, je dois alors étendre Animal et Walking. Ce qui m'oblige à définir la méthode walk pour cet animal. Après quoi, je peux appeler la fonction walk(Walking) pour les canards.

L'approche orientée objet permet clairement de s'éviter des problèmes à la source et d'avoir moins de cas d'erreur à gérer. On gagne en flexibilité sur la mise en place de nouveaux comportements associés à une opération. Mais en contrepartie, on perd en flexibilité sur l'ajout de nouvelles opérations : si le code ci-dessus fait partie d'une bibliothèque scellée, je ne pourrais pas ajouter un comportement dance, sauf à le faire en partant sur la solution précédente ou en utilisant d'autres approches basées sur la réflexion. Autre point : Dog et Dinosaur est toujours associé au mixin Walking quelque soit le contexte. On peut imaginer des contextes dans lequel "Dinosaur étend Walking" n'a aucune utilité, par exemple lorsque le dinosaure dort (somnambule), ou est perturbant, par exemple lorsqu'il est décédé (walking dead).

C'est vraiment perturbant !

C'est vraiment perturbant !

Les typeclasses offrent une réponse à ces cas problématiques.

Typeclasse

Les typeclasses permettent de catégoriser des types existant dans des contextes bien précis et d'associer à ces types des opérations communes.

La mise en place de typeclasses au sein de Scala passe par un idiome basé sur la notion de déclaration implicite. Comme le principe n'est pas très connu, nous allons procéder par étape.