Un coup d'oeil sur la composition de PL par les coroutines de Kotlin

这篇文章并非使用中文书写。您可以用Google Translator来辅助阅读。

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
Exception in thread "main" java.lang.IllegalStateException: Already resumed

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.


Un coup d'oeil sur la composition de PL par les coroutines de Kotlin
http://inori.moe/2024/02/19/composition-pl-kotlin-fr/
作者
inori
发布于
2024年2月19日
许可协议