Publié par

Il y a 5 ans -

Temps de lecture 9 minutes

Troubles with types

En attendant avec impatience le cru 2014 de la conférence Scala.IO, j’ai récemment regardé la présentation ‘Trouble with Types’, de Martin Odersky. Elle a eu lieu lors de la conférence Strange Loop l’année dernière. L’inventeur du langage Scala propose de parler de théorie des langages et surtout, des systèmes de type. Car finalement, pourquoi a-t-on besoin de type?

 

Petit rappel

Il existe un premier découpage des systèmes de type, dynamique ou statique. Il nous est rappelé les points forts des deux écoles.

Typage statique

  • Plus efficace à l’exécution
  • Outillage de développement plus précis
  • Moins de tests nécessaires, des contraintes sont vérifiées à la compilation
  • Le typage sert de documentation
  • Permet du refactoring pour la maintenance

Typage dynamique

  • Le langage en lui-même est plus simple
  • Plus expressif et moins verbeux
  • Pas de problème de compilation
  • Plus adapté à l’exploration

Qu’est-ce qu’un bon design?

Martin enchaîne rapidement sur cette épineuse question. Pour lui, il doit être :

  • clair
  • correct
  • minimal

On retrouve ici les principes des designs évolutifs avec les deux acronymes YAGNI (You Ain’t Gonna Need It) et KISS (Keep It Simple, Stupid).

La recette de Martin pour construire un bon design? Patterns & Constraints.

Comment un langage peut-il permettre de faire un bon design? Tout simplement en permettant de le découvrir. Petite citation du jour : « Great designs are often discovered, not invented ». Pour lui, les « patterns » s’expriment au travers des « abstractions », les « contraintes » par les « types ».

Des types pour exprimer des contraintes

Et donc voilà pourquoi on a besoin de types ! Pour y exprimer des contraintes métier. La valeur de ma transaction financière est un réel, pas une chaine de caractère, mon nom de famille est composé de caractères, pas de chiffres… Et cela fait bien partie de notre travail de développeur, traduire les contraintes de problèmes réels dans un langage compréhensible par la machine.

Ce qui va différer entre les deux approches, c’est le niveau de sécurité que l’on veut avoir et à quel moment.

Dans un langage au typage dynamique, ces contraintes sont vérifiées à l’exécution, c’est à dire au plus tard dans le cycle de vie d’un programme. C’est une approche très pragmatique. Il permet des cycles de développement cours et est très adapté à l’exploration. Sa philosophie : What You See Is What You May Get. En effet, sans tests automatisés, impossible d’avoir la sécurité que les différents modules s’intègrent bien les uns avec les autres.

Au contraire, avec un typage statique, le développeur va s’assurer que son programme s’intègre bien au travers de contrats. Ces contraintes sont vérifiées avant même que le programme puisse se lancer. Sa philosophie : What You See Is What You Try To Get. En effet, où placer une contrainte métier : à la compilation dans le type ou à l’exécution par validation? C’est peut-être là LA différence principale entre un développeur habitué à l’orienté objet et un plus familiarisé avec la programmation fonctionnelle.

Avec la programmation fonctionnelle, le typage se veut souvent statique

Issue majoritairement du milieu universitaire, la culture mathématique a induit la volonté de placer un maximum de contraintes dans le type des données et ses abstractions. Les niveaux d’abstractions sont parfois si élevés qu’un développeur habitué à résoudre des problèmes plus « concrets » peut être perdu.

Imaginons la méthode suivante sur un Double.

public Double divisePar(double diviseur){
 if(diviseur == 0){
  throw new IllegalArgumentException("Le diviseur ne peut être 0");
 }

 return this / diviseur;
}

Pourquoi utiliser un double pour le diviseur ? Cette fonction n’est pas définie pour l’ensemble des valeurs de son paramètre en entrée. Je suis obligé de faire une vérification de la valeur à l’exécution, qui se traduit par une exception. Pourquoi ne pas créer un type de donnée similaire au double mais sans 0 ?

Imaginons la méthode suivante sur un Double.

//NonZero est difficile à exprimer en Java
//NonZero un = NonZero(1);
//NonZero(0); //throw new IllegalArgumentException
 
public Double divisePar(NonZero diviseur){
 return this / diviseur;
}

À l’exécution, ces deux propositions sont identiques, une exception sera lancée. Mais avec la seconde solution, la méthode annonce clairement son contrat. Si l’on désire l’utiliser, c’est à l’utilisateur de cette méthode de s’ assurer que le diviseur est différent de zéro.

C’est ce type de réflexion que la programmation sur les types nous amène à nous poser, et il faut avouer que c’est plutôt déroutant.
Le type d’une donnée joue aussi le rôle de documentation. Si je sais qu’en retour d’une méthode, je peux ne rien retourner, pourquoi utiliser null ? Il vaut mieux utiliser le système de type pour traduire cette contrainte. C’est le but de la « classe » Option. J’inscris dans mon contrat, via un type, que je peux ne rien retourner. C’est vérifié à la compilation et l’utilisateur de mon API doit gérer le cas explicitement.

C’est de là que vient toute la problématique avec les types. Nous en avons besoin si nous souhaitons construire des systèmes simples, efficaces et durables. Mais bien le faire est difficile. Il existe des debuggeurs pour vérifier les contraintes de notre programme à l’exécution. Cependant, il n’y a pas de debuggeur quand le compilateur n’arrive pas à faire son travail de vérification des types. Petit extrait d’une exception du compilateur Scala sur un exemple bien connu :

5862.scala:36: error: type mismatch;
found : scala.collection.mutable.Iterable[_ >: (MapReduceJob.this.DataSource, scala.collection.mutable.Set[test.TaggedMapper[_, _, _]]) with test.TaggedMapper[_$1,_$2,_$3] forSome { type _$1; type _$2; type _$3 } <: Object] with scala.collection.mutable.Builder[(MapReduceJob.this.DataSource, scala.collection.mutable.Set[test.TaggedMapper[_, _, _]]) with test.TaggedMapper[_$1,_$2,_$3] forSome { type _$1; type _$2; type _$3 },scala.collection.mutable.Iterable[_ >: (MapReduceJob.this.DataSource, scala.collection.mutable.Set[test.TaggedMapper[_, _, _]]) with test.TaggedMapper[_$1,_$2,_$3] forSome { type _$1; type _$2; type _$3 } <: Object] with scala.collection.mutable.Builder[(MapReduceJob.this.DataSource, scala.collection.mutable.Set[test.TaggedMapper[_, _, _]]) with test.TaggedMapper[_$1,_$2,_$3] forSome { type _$1; type _$2; type _$3 },scala.collection.mutable.Iterable[_ >: (MapReduceJob.this.DataSource, scala.collection.mutable.Set[test.TaggedMapper[_, _, _]]) with test.TaggedMapper[_$1,_$2,_$3] forSome { type _$1; type _$2; type _$3 } <: Object] with scala.collection.mutable.Builder[(MapReduceJob.this.DataSource, scala.collection.mutable.Set[test.TaggedMapper[_, _, _]]) with test.TaggedMapper[_$1,_$2,_$3] forSome { type _$1; type _$2; type _$3 },scala.collection.mutable.Iterable[_ >: (MapReduceJob.this.DataSource, scala.collection.mutable.Set[test.TaggedMapper[_, _, _]]) with test.TaggedMapper[_$1,_$2,_$3] forSome { type _$1; type _$2; type _$3 } <: Object] with scala.collection.mutable.Builder[(MapReduceJob.this.DataSource, scala.collection.mutable.Set[test.TaggedMapper[_, _, _]])

et cela continue sur 200 lignes ;)

Martin le précise dans son discours. Programmer dans le système de type est séduisant, mais reste très peu outillé, complexe car abstrait.

Et JavaScript et Clojure alors ?

Ces deux langages marquent clairement la démarcation entre programmation fonctionnelle et typage statique. Ces langages sont fonctionnels car la fonction est une primitive du langage. Par contre, ils sont dynamiquement typés. Ces langages sont puissants et flexibles. Clojure est d’ailleurs un des langages préférés par la communauté DDD (Domain Driven Design). Tout est immutable et l’on se focalise sur les transformations des données, les fonctions.

Cependant, l’absence de types statiques en fait souvent la cible de critiques de la part des partisans du camp opposé. De la même manière, les temps de compilation de Scala sont tout aussi souvent critiqués.

Jusqu’à récemment, je n’avais que peu d’expérience avec ces langages dynamiques, étant plutôt adepte de langages comme Java, Scala ou Haskell. Notre Hackathon chez Xebia m’a permis de travailler avec Node.JS en JavaScript. Débuter a été une expérience déroutante. Fini ce fil d’Ariane qui me permet de savoir ce que je manipule, ce que je fait. Je présume. À mon étonnement, cela fonctionne plutôt bien.

Conclusion

Le typage statique, c’est majoritairement le choix de la pérennité. Cela permet de transposer un maximum de contraintes métiers dans les types. Le compilateur se charge pour nous de les vérifier avant l’exécution. Cela nous évite une batterie de tests d’intégrations des modules. Le refactoring est aussi plus sûr. En contrepartie, il est souvent plus fastidieux d’écrire du code expressif. Le raisonnement n’est pas aisé et peu induire beaucoup d’erreurs.

À l’opposé, ce n’est pas un hasard si la majorité des startups développent leurs produits dans des langages au typage dynamique comme Ruby avec la plateforme Rails ou Javascript avec Node.js. D’ailleurs, la majorité des projets réalisés lors du Hackathon a choisi Node.JS pour serveur. Cela permet souvent d’aller plus vite et se concentrer sur le produit. Ces langages sont aussi d’excellents supports pour créer des DSL de tests. Mais il s’ensuit quasiment systématiquement une migration vers un langage statiquement typé pour des problématiques de performance et de maintenance. Twitter en est un bon exemple.

Petit bonus de la présentation, Martin nous présente rapidement Dotty, son nouveau langage. Il prototype un nouveau système de typage qui sera certainement l’avenir de Scala. Il tend à simplifier les différentes abstractions du système de typage. Scala fait le pari de lier l’expressivité et la puissance des langages dynamiques et la sécurité d’un système de type statique, au prix d’une compilation au coût qui peut sembler exorbitant. Mais finalement, qu’est-ce qu’une compilation sinon qu’une suite de tests, lancée avant l’exécution, qui s’assure du bon fonctionnement de l’application ? Vous préférez attendre la phase de tests ou le lancement d’Infinitest ?

En écrivant cet article, je pense à une dernière citation. Pour moi, un système de types dynamiques permet de construire un programme qui n’est pas cassé. Avec un système de types statiques, le programme est incassable. Que l’on choisisse de travailler encore plus avec le compilateur ou de s’en passer, produire un design qui sera clair et minimal, cela demande de la maîtrise et beaucoup, beaucoup, beaucoup de pratique.

Et demain, allez-vous typer un peu, beaucoup, à la folie, ou pas du tout ?

Pour aller plus loin

Vous souhaitez faire votre choix, nous vous conseillons les liens suivants.

Publié par

Publié par Xavier Bucchiotty

Software Engineer Scala Fanboy FP constant learner Akka trainer

Commentaire

2 réponses pour " Troubles with types "

  1. Publié par , Il y a 5 ans

    En effet, les types sont un excellent moyen de documentation du code.
    Donner explicitement le type de sortie et des paramètres d’une fonction est souvent obligatoire ou chaudement recommandée (même en Haskell).
    Par contre, entre ces deux propositions, la double répétition du type en Java gêne plus qu’il n’informe.
    var array = [];
    List list = new ArrayList<>();

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Nous recrutons

Être un Xebian, c'est faire partie d'un groupe de passionnés ; C'est l'opportunité de travailler et de partager avec des pairs parmi les plus talentueux.