J’adore kotlin. En particulier parce qu’il me permet de limiter le bruit du langage, et de focaliser sur le fonctionnel et les intentions.
Pour illustrer ça, jouons au Jeu de la vie. C’est une manière simplifiée de simuler les comportements émergents de la vie. En particulier, au cœur de ce simulateur, on trouve les règles suivantes :
- une cellule morte possédant exactement trois cellules voisines vivantes devient vivante (elle naît), sinon elle reste morte.
- une cellule vivante possédant deux ou trois cellules voisines vivantes le reste, sinon elle meurt.
En java, j’aurais probablement écrit ce genre de code pour implémenter ces règles :
[code lang= »java »]
public Boolean willLive(Boolean didLive, int numberOfLiveNeighbours) {
if (didLive) {
if (numberOfLiveNeighbours == 2 || numberOfLiveNeighbours == 3)
return true;
else
return false;
}
else if (numberOfLiveNeighbours == 3)
return true;
else return false;
}
[/code]
En kotlin, je suis arrivé à du code comme ça, qui me semble plus clair :
[code lang= »java »]
fun willLive(didLive: Boolean, numberOfLiveNeighbours: Int): Boolean =
when (didLive surroundedBy numberOfLiveNeighbours) {
true surroundedBy 2, true surroundedBy 3 -> true
false surroundedBy 3 -> true
else -> false
}
private infix fun Boolean.surroundedBy(numberOfLiveNeighbours: Int) =
Pair(this, numberOfLiveNeighbours)
[/code]
J’y suis notamment arrivé grâce à la méthode d’extension infix surroundedBy, qui crée une instance de Pair. Elle m’a permis de mettre un petit peu plus en évidence le domaine, et surtout de limiter les niveaux d’imbrication.
Et pourtant, j’aurais aimé écrire un autre code :
[code lang= »java »]
fun willLive(didLive: Boolean, numberOfLiveNeighbours: Int): Boolean =
when (didLive, numberOfLiveNeighbours) {
(true, 2), (_, 3) -> true
else -> false
}
[/code]
C’est le genre de code que j’aurais pu écrire en python, rust, scala, ou elm, et probablement beaucoup d’autres langages. Il met en œuvre deux fonctionnalités :
- La possibilité de passer plusieurs arguments à when, voire d’y déconstruire un tuple ou un objet.
- Le pattern matching sur plusieurs arguments. Ici, par ex, j’ai dit que je voulais qu’une paire de n’importe quel état et de 3 voisines vivantes donne une cellule vivante.
Kotlin n’offre ni l’une ni l’autre, et c’est une de ses grosses limites, qui frustre les devs qui viennent de langages plus complets de ce point de vue. Ça oblige à passer par des circonvolutions comme la méthode surroundedBy ci-dessus.
Et ça limite grandement les possibilités de refactoring progressif. En effet, le code auquel je suis naturellement arrivé par TDD en kotlin ressemble finalement au code java qu’on a vu plus haut :
[code lang= »java »]
fun willLive(didLive: Boolean, numberOfLiveNeighbours: Int): Boolean =
if (didLive) when (numberOfLiveNeighbours) {
2, 3 -> true
else -> false
} else when (numberOfLiveNeighbours) {
3 -> true
else -> false
}
[/code]
Si kotlin avait proposé ces possibilités, il aurait été facile pour intellij de détecter les if/when imbriqués sur les mêmes variables, et de proposer de les fusionner directement en un seul when. Puis de fusionner (true, 3) et (false, 3) en (_, 3). Sans ces fonctionnalités, il ne peut pas. C’est d’autant plus dommage que kotlin a été créé pour maximiser le refactoring automatique dans un IDE.
Pour arriver au premier morceau de code que je vous ai montré, j’ai dû abandonner les baby-steps automatiques, et monter une trop grosse marche à mon goût. J’obtiens une branche de trop dans le when. Et j’ai cette méthode surroundedBy qui sert de vitrine à des fonctionnalités intéressantes de kotlin, mais qui amène une complexité artificielle dont je me serais bien passé.
Dommage. Je pense que kotlin va devoir rapidement apporter ces fonctionnalités, surtout maintenant que java a décidé de s’y atteler. Même si ça va probablement prendre plusieurs années pour java.