deft/notes/2021-01-03--12-08-46Z--voeux_2021_alexandre.org
Yann Esposito (Yogsototh) d641cd04d9
moved files
2021-09-15 09:12:29 +02:00

7.2 KiB
Raw Blame History

voeux 2021 alexandre

tags
source

Bonjour Alexandre, je te souhaite à toi et toute ta famille une très bonne année 2021 !

Je vous souhaite à tous et à votre famille une très bonne santé surtout dans le climat actuel. De trouver un bon équilibre entre la vie familiale, les passions et le travail.

Et je me permet d'ajouter à ce mail une réflexion technique entre Haskell et Clojure qui je l'espère t'inspirera.

Bien que je n'écrive plus vraiment de nouvel article sur Haskell/Purescript, j'ai passé pas mal de temps à réfléchir à comment architecturer du code dans ces langages et Clojure. J'ai l'impression qu'une solution qu'on utilise actuellement en Clojure bien qu'à première vue très inférieure aux solutions proposées par Haskell/Purescript sont en fait supérieure lorsqu'on les regardes selon le bon angle.

L'idée de fond c'est que les systèmes d'effets/Free Monad (et les sous-systèmes, style MTL, handler pattern) sont des cas particuliers restreints de ce que l'on peut obtenir avec un système de "Services".

Si on regarde l'intérêt de ces systèmes d'organisation de code le plus évolué étant les Free Monads et les systèmes d'effets leur objectif est toujours plus ou moins le même.

Changer le comportement du même bloc de code en fonction du contexte d'exécution. En gros si on prend une function qui ressemble à:

my_function arg1 ... argn = do
  x1 <- action1 arg1 .. argn
  x2 <- action2 arg1 .. argn x1
  ...
  z <- actionK arg1 .. argn x1 .. x<K-1>
  return z

les action1 à actionK auront des comportement différents en fonction du contexte dans lequel il va être initialisé. Dans le cas de Haskell, on fait au mieux pour déterminer le contexte à compile time en utilisant les types. Du genre:

main = do
  effect1 <- initEffect1
  effect2 <- initEffect2 effect1
  ...
  effectM <- initEffectM effect1 ... effect<M-1>
  runWithEffects [effect1,effect2,...,effectM] my_function

Ce système présente ainsi beaucoup d'avantanges:

  1. mettre toute la "business logic" dans la fonction my_function en se débarrassant des détails techniques au mieu en les envoyant dans les "Effets".
  2. Pouvoir facilement changer les Effects par des Effects purs et pouvoir ainsi écrire des tests déterministes et reproductibles aisément.
  3. Controller "par le haut" les sous-effets disponibles dans certaines branches du code. On peut du coups facilement contraindre et voir que certaines fonction ne pourront jamais accéder à la DB par exemple.

Par contre ce système à un coût sur les performances. Je pense que le coût est négligeable dans la majorité des cas et qu'il s'agit d'"Haskell circle jerking" lorsque les gens refusent d'utiliser des systèmes d'effects pour cette raison.

Si on va au bout de l'utilisation de tel système en réalité on voit qu'il faut, plutôt qu'utiliser des systèmes tout prêt à l'emploi, se créer beaucoup d'effects maison pour en tirer le meilleur parti.

Typiquement on va avoir des effects avec leur pendant pur qui devraient probablement être standards comme un effect de Log, d'accès complet à une DB peut-être ? Mais assez vite, on a envie de se fabriquer des Effets spécialisé au domaine de son application. Typiquement, si on fait de la gestion d'utilisateur on va créer un effet pour la gestion d'utilisateurs plutôt que filer un Effet complet d'accès à la DB. Ce qui permet de s'assurer que des fonctions n'auront accès qu'aux tables de la DB utilisées pour les utilisateurs et pas pour les tables destinées à d'autres buts etc…

Et c'est là qu'apparaît la première limitation des systèmes d'Effets. Les effets ont des dépendances entre eux. L'effet UserEffect doit dépendre de l'effect DBEffect. Mais probablement aussi de l'effet qui permet d'écrire des logs, et de pas mal d'autres. Et c'est à la lumière de voir ces effects comme des outils d'applications pragmatiques et pas juste des "Effect algébriques" que l'on voit apparaître un besoin de gestion de ces dépendances. Un aspect important devient donc la gestion de l'ordre d'init et de stop de tous ces effets. En effet, pour faire initUserEffect il faudra lui passer en paramètre des effets dejè instanciés (et donc initialisés) pour les logs et la DB.

Un premier problème est donc que changer/modifier des dépendances entre effet dans un point du système revient à imposer une modification manuelle du code dans tous les effets dépendants. Mais aussi en particulier le code de l'init du système devient lourd à gerrer alors qu'il existe une solution simple. Je pense que Tardis pourrait être utile dans ce cas, mais, ça ne me semble pas encore assez facile. Même avec tardis, il me semble qu'il faut savoir si un effet doit arriver avant ou après. Là on souhaiterai pouvoir utiliser la lazyness pour une gestion automatique des dépendances.

En réalité l'API la plus souhaitable serait de laisser la gestion de l'ordre des dépendance et leur appel laissé à un système automatique. Ainsi au lieu d'avoir

main = do
  confEffect <- initConfEffect "config.dhall"
  dbEffect <-  initDBEffect confEffect
  userEffect <- initUserEffect dbEffect confEffect
  runWithEffects [confEffect,dbEffect,userEffect] myFunction

Il semble préférable d'écrire:

main = do
  let effects = [ ConfEff, DBEff, UserEff ]
  runWithEffects effects myFunction

Sans avoir à se poser la question de quel effet doit utiliser quel autre. Ce sont des détails qui doivent être dans les Effets eux-même et pas donné à une gestion manuelle par l'utilisateur.

Et c'est exactement ce que les systèmes de services font en clojure. Il en existe plusieurs. Component 1 et Trapperkeeper 2. Je travaille avec le second.

Evidement en Clojure tout se gère en runtime. Mais si j'ai bien lu et compris la présentation sur les différents systèmes d'Effets il semble difficile de faire beaucoup mieux en Haskell. Il est en effet très difficile de faire descendre dans le code le détail qui permet de savoir dans quel contexte il sera exécuté et changer le code pour utiliser seulement le bon contexte et l'optimiser en conséquence.

Finalement, j'ai vu des essais pour exploiter ces idées en Haskell. Mais elles sont toutes faiblardes et échouent. J'ai moi-même essayé. Le probléme de fond vient du fait qu'il est quasi impossible de trouver un type qui soit à la fois assez generique pour matcher ce que doit faire un Service et assez précis pour être utile lors du typage du code.

Voici donc un point qui m'agace au plus haut point. D'habitude je préfère défendre les approches à la Haskell. Mais pour le coups, impossible de faire aussi bien que Clojure et de plus sur un des sujets les plus chauds actuellement dans la communauté.

Peut-être que l'écriture de cet email me donnera l'élan nécessaire pour communiquer cette constation et le publier sur r/haskell :).

Quoi qu'il en soit. Je te souhaite encore une très bonne année 2021 !

Yann.