Un coup d'oeil sur la composition de PL par les coroutines de Kotlin
J'ai récemment révisé Kotlin et ses coroutines. Par coïncidence, j'ai rencontré un exercice (Tricky Kotlin #8: Simple for-comprehension) assez intéressant dans un article, donc j'ai un peu travaillé dessus.
Dans un premier temps, c'est un exercice avec un peu de difficulté
parce que l'objectif est d'implémenter une syntaxe de
do-notation
dans Kotlin, comme celle de Haskell. Cependant,
avec des indications dans l'article sur une base de Kotlin/coroutines et
la programmation fonctionnelle, ce n'était pas si dur comme on le
croyait. L'idée est de suspendre et reprendre les
coroutines pour simuler la composition monadique. Je ne vais pas rentrer
trop en détail pour ne pas divulguer la solution. Bien évidemment, les
Optional
et Nullable
sont monadiques en soi,
donc il n'y a pas trop de secrets dedans.
Peut-être, c'est un bon exemple pour expliquer comment « les monades ne sont que des contextes qui peuvent être composés ».
Puis, j'ai trouvé un thread concernant l'avantage des coroutines [zh] de Kotlin par rapport aux autres langages. En général, dans Kotlin, on peut d'or et déjà implémenter plusieurs features avec une seule primitive, i.e. les coroutines. Ce n'est pas le cas dans tous les langages. D'ailleurs, le fait que Kotlin implémente les CPS transformations en compile time n'empêche pas les utilisateurs-développeurs de fournir leurs propres dispatchers ou des abstractions des thread-pools et/ou threading libs adaptées en fonction des cas d'utilisation variés.
Je me souviens des querelles concernant les stackful ou stackless coroutines, et le fameux Project Loom qui occupe les médias depuis des mois. Il me semble que Kotlin est un bon exemple pour montrer la composition et l'orthogonalité des features d'un langage. C'est ainsi pourquoi la composition compte [zh].
- On ajoute les coroutines (construites à partir de la continuation), donc les control flows deviennent des expressions, pour représenter des notions asynchrones ou multi-threading difficiles à exprimer dans d'autres langages. On peut même tenter d'exprimer des concepts de FP en dehors de OOP classique.
- En revanche, on s'éloigne de la mutation et la nullabilité. Ça nous permet d'écrire des codes qui ont plus de sens et ça rend nos codes théoriquement raisonnables. De la même façon, ça facilite le parallélisme et les multi-threadings. Bien que le TDD soit puissant, ce sont quand même des choses que le TDD n'arrive pas à faire.
D'un point de vue plus général, les nullables (ou optionals), les collections, les boîtes, les tâches, ce sont des objets avec quelques attributs en commun, même si ce n'est pas évident d'un coup d'œil (si on évite de dire monad par ici). Ça nous permet alors de pousser l'abstraction pour aller plus loin et profiter d'une expressivité plus riche, même si on reste dans les limites de l'OOP et du subtyping.
Pourtant, comme un langage OOP assez pragmatique, la composabilité de
Kotlin est quand même limitée. Reprenons l'exemple ci-dessus, mais si on
remplace le type Optional
par List
, l'erreur
va être déclenchée dès qu'on retire un deuxième élément :
1 |
|
La raison est simplement parce que la coroutine de Kotlin est
single-shot, or, on rentre dans le non-déterminisme quand on utilise les
structures comme List
qui peuvent sortir plusieurs
possibilités. Certaines bibliothèques de FP peuvent proposer de
réimplémenter une coroutine multishot. Mais c'est un peu hors sujet de
ce kata de Codewars.
Avec cette limitation, il est quand même possible d'impémenter un
for-comprehension
pour des containers monadiques qui ne
contiennent qu'un élément, comme Either
ou
Result
(et possiblement IO
?).
It's not possible at the moment to implement monad comprehension for List, Flow, and other non-deterministic data structures that emit more than one value. The current implementation of continuations in Kotlin is single shot only. This means a continuation can resume a program with a single emitted value.
Voici une explication de cette issue sur StackOverflow.
Une autre limitation est liée au subtying. En fait, même si on
suppose qu'il existe une for-comprehension
pour les listes.
On peut ensuite constater que nous avons justement une implémentation
pour chaque type concret. Or, 90% de code rest quasiment identique. Les
développeurs ont certainement envie de ne maintient qu'une seule
implémentation qui peut servir partout.
Cependant, ce n'est pas possible dans Kotlin. Parce que les types
comme List ou Optional sont builtins par défault, et ils ont des types
de base ou interfaces différents. Plus concrètement,
Optional
est un type de Java, bien évidemment, les
programmeurs Java n'ont pas envie d'étendre le type Optional de la base
Collection.
Peut-être, on peut se demander si on peut donner la possibilité aux
utilisateurs d'étendre le type Optional
de
Collection
? Certains langages comme Swift le permettent
partiellement. Mais on revient à la question précédemment posée : cette
feature permettra-t-elle d'augmenter la composibilité ?
La réponse n'est pas si claire.