4 points par GN⁺ 2023-08-20 | 2 commentaires | Partager sur WhatsApp
  • Railway Oriented Programming est une approche qui traite, au sein d’un pipeline fonctionnel, les chemins d’échec récurrents dans les applications réelles, comme la validation, la journalisation ou les erreurs réseau
  • Le point clé est d’expliquer, à l’aide de la métaphore du railway, les limites des exemples qui n’enchaînent que le chemin de réussite, afin que même les utilisateurs de F# puissent visualiser et comprendre facilement le flux d’erreurs
  • Du point de vue de Haskell, cela se rapproche du type Either et de la composition de Kleisli, mais l’accent est mis sur les recettes de gestion d’erreurs et les patterns d’implémentation plutôt que sur la théorie abstraite
  • Pour une application concrète, la bibliothèque F# Chessie pour NuGet, des exemples GitHub comparant C# et F#, ainsi qu’un exemple appliqué à FizzBuzz sont fournis
  • C’est une approche utile, mais il ne faut pas l’appliquer excessivement à tous les problèmes ; comme F# n’a pas de type class, la bibliothèque d’exemple définit directement les fonctions nécessaires

Supports de présentation et code d’exemple

  • La page Railway Oriented Programming rassemble en un seul endroit les slides et le code de la présentation
  • Le point de départ est le constat qu’il est difficile de construire des applications réelles avec les seuls exemples de programmation fonctionnelle qui supposent uniquement une « happy path »
  • Une application robuste doit aussi gérer la validation, la journalisation, les erreurs réseau, les erreurs de service et d’autres situations exceptionnelles

Vidéo et slides

  • Ce sujet a été présenté à NDC London 2014
  • D’autres vidéos de la même présentation sont également disponibles
  • Les slides de la présentation du 14 mars 2014 à Functional Programming eXchange sont disponibles
  • Les slides PowerPoint peuvent aussi être téléchargées sur GitHub, avec l’indication qu’elles peuvent être réutilisées librement

Monad Either et composition de Kleisli

  • Pour les utilisateurs de Haskell, cette approche peut ressembler au type Either
  • Ici, elle est spécialisée sous une forme où le cas Left contient une liste de types d’erreurs personnalisés
    • Le type d’exemple est type TwoTrack a b = Either [a] (b,[a])
  • Ce qui a été créé n’est pas tant le concept de gestion d’erreurs lui-même que la métaphore du railway servant à l’expliquer
  • La raison de ne pas mettre en avant la terminologie standard de Haskell est claire
    • Ce contenu n’est pas un tutoriel sur les monades, mais se concentre sur la résolution de problèmes de gestion d’erreurs
    • Beaucoup de personnes qui découvrent F# ne sont pas familières des monades ; une explication visuelle et moins intimidante peut donc être plus intuitive
    • Appeler directement « monade » un type à deux pistes doté de bind serait imprécis, et les lois des monades ne sont pas abordées
    • Either est un outil trop général ; l’objectif ici est de fournir une recette plutôt que de s’attarder sur l’outil lui-même

Les composants de la recette de gestion d’erreurs

  • Cette approche est un ensemble de techniques plus large que le simple fait de « prendre Either et bind »
  • Parmi les techniques incluses :
    • l’utilisation, des deux côtés, de listes de types d’erreurs personnalisés plutôt que d’une forme simple comme Either String a
    • l’intégration de fonctions monadiques dans un pipeline avec bind(>>=)
    • la composition de fonctions monadiques avec la composition de Kleisli (>=>)
    • l’intégration de fonctions non monadiques dans un pipeline avec map(fmap)
    • comme F# n’utilise pas de monade IO, l’intégration de fonctions unit dans un pipeline via tee
    • la conversion des exceptions en cas d’erreur
    • la combinaison en parallèle de fonctions monadiques avec &&&, par exemple pour des validations
    • l’exploitation des avantages qu’apportent les types d’erreurs personnalisés dans une approche de domain-driven design
    • des extensions comme la journalisation, les événements de domaine ou les transactions de compensation
  • Le but est de créer un template suffisamment polyvalent pour être utilisé dans presque toutes les situations, tout en étant assez contraignant pour imposer un style cohérent
  • Si la façon d’écrire le code converge pratiquement vers un seul schéma, il devient plus facile pour les personnes qui assureront la maintenance de comprendre rapidement l’organisation du code
  • Ce n’est pas l’unique solution possible, mais cela peut constituer un bon point de départ
  • Même dans la communauté Haskell, les approches de gestion d’erreurs ne sont pas uniformes, ce qui peut dérouter les débutants

Comment l’appliquer dans mon code

  • Si vous avez besoin d’une bibliothèque F# prête à l’emploi, vous pouvez consulter le projet Chessie utilisable avec NuGet
  • Un exemple de service web appliquant cette technique est fourni dans ce projet GitHub
  • Un exemple appliquant l’approche ROP à FizzBuzz est disponible dans Railway Oriented Programming: carbonated
  • F# ne dispose pas de type class, il n’existe donc pas de méthode générale pour réutiliser les monades
  • La bibliothèque FSharpX propose une approche utile, mais la bibliothèque Rop.fs redéfinit directement toutes les fonctions à partir de zéro
  • Cet isolement présente l’avantage de ne dépendre d’aucune dépendance externe

Pour aller plus loin

2 commentaires

 
GN⁺ 2023-08-20
Avis sur Hacker News
  • Elixir traite ce problème assez proprement avec le mot-clé/la macro with
    Comme dans with {:ok, file_handle} <- File.open(filename), ... do {:ok, parsed} end, les fonctions sont exécutées de haut en bas et, si chaque valeur de retour ne correspond pas au motif de gauche, cette valeur est renvoyée immédiatement
    Ainsi, au lieu de devoir faire en sorte que chaque fonction accepte à la fois {:ok, value} et {:error, reason}, la fonction peut ne recevoir que la valeur qui l’intéresse, et laisser le pattern matching du bloc with gérer la propagation des erreurs
    Par exemple, si File.open renvoie {:error, reason}, IO.read n’est pas exécuté et le résultat global du with devient {:error, reason}. Au final, on n’écrit que le chemin de succès, et on peut laisser l’appelant matcher le chemin d’échec s’il le souhaite

    • Si « ce qui ne matche pas est renvoyé immédiatement », je me demande ce qui se passe quand ces valeurs sont toutes de types différents. Avec un typage dynamique, j’ai l’impression qu’il faudrait regarder de quel type il s’agit pour deviner où ça a échoué
      Ça donne un peu l’impression de réinventer à moitié la gestion des exceptions ; autant l’appeler else plutôt que catch, et ça ressemblerait simplement moins à de la gestion d’exceptions
    • À ma connaissance, c’est une forme influencée par le where de Haskell et le let de Lisp
      C’est en quelque sorte une manière de définir les préconditions d’une fonction, et dans Elixir on peut en plus utiliser un bloc else
    • Je me demande en quoi c’est mieux que les exceptions. Et si IO.read échoue, la question est aussi de savoir qui ferme le handle du fichier
  • L’auteur a aussi écrit quelques années plus tard un billet de suivi intitulé Against Railway Oriented Programming : https://fsharpforfunandprofit.com/posts/against-railway-orie...
    La programmation orientée « railway » est intéressante et a ses cas d’usage, mais elle mérite, à mon avis, une mise en garde importante. En pratique, je l’ai souvent vue servir à réinventer la gestion des exceptions en moins bien ; alors qu’en comprenant et en utilisant correctement les exceptions, on peut traiter la plupart des conditions d’erreur de manière plus propre et plus efficace.
    L’avantage des exceptions, c’est que dans la plupart des cas, le choix sûr est le comportement par défaut. Une condition d’erreur signale que le code ne peut pas faire ce que sa spécification promet, et continuer dans cet état risque de corrompre les données sur la base de mauvaises hypothèses. Par défaut, une exception déclenche le nettoyage puis se propage à l’appelant, tandis que des approches comme la programmation orientée « railway » exigent beaucoup de boilerplate facile à oublier et à mal écrire.
    Cela dit, elle est utile dans deux cas. Le premier, c’est lorsque des erreurs attendues, spécifiques et bien définies doivent être traitées au point où elles se produisent, comme dans la validation. Le second, c’est dans les environnements où l’on ne peut pas utiliser la gestion des exceptions. L’ancien code asynchrone à base de Promises de jQuery ressemblait presque à une implémentation de programmation orientée « railway », mais avec le JavaScript moderne et async/await, on peut de nouveau utiliser la gestion des exceptions.

    • Ce qui me gêne dans les exceptions des langages grand public, c’est que ce traitement se fait de façon invisible. Il n’est pas facile de savoir à la compilation quelles exceptions peuvent sortir d’une fonction donnée.
      Le grand bénéfice de la programmation orientée « railway », c’est qu’en regardant simplement la signature d’une fonction, on sait immédiatement quelles erreurs peuvent en sortir.
      Le boilerplate peut être atténué selon le langage. Haskell a la notation do, et en F# on peut écrire cela très proprement avec les expressions de calcul pour les résultats. Par exemple, on peut définir quelque chose comme LoginError = InvalidUser | InvalidPwd | Unauthorized of AuthError, puis construire le flux avec Result.requireSome, Result.requireTrue et Result.mapError.
      À l’inverse, je me demande s’il est possible d’atténuer aussi les défauts des exceptions. J’aimerais savoir s’il existe des linters ou outils d’analyse statique intégrés aux IDE Java capables d’afficher automatiquement les exceptions non interceptées susceptibles d’être lancées depuis une ligne de code donnée.
      https://demystifyfp.gitbook.io/fstoolkit-errorhandling/fstoo...
    • Je trouve que la manière traditionnelle d’implémenter les exceptions est assez médiocre.
      D’abord, beaucoup de langages imposent des scopes maladroits et inutiles pour intercepter les exceptions. Par exemple, si l’on veut déclarer et initialiser une variable avec le résultat d’une fonction susceptible de lancer une exception, puis lui donner une autre valeur en cas d’échec, on se retrouve à devoir séparer la déclaration de l’initialisation.
      Le problème plus important, c’est qu’il faut traiter les conditions d’erreur au niveau le plus bas disposant de suffisamment de contexte pour le faire correctement, alors que le comportement par défaut des exceptions est de « remonter tout en haut ». Il suffit d’ajouter un cache de fichiers pour que la fonction HandleRequest de plus haut niveau puisse soudain lancer des exceptions d’I/O : l’abstraction se met facilement à fuir.
      À mon avis, tout ce qu’une fonction peut retourner devrait faire explicitement partie de sa signature, et l’appelant devrait être obligé soit de le traiter explicitement, soit d’indiquer qu’il le propage vers le haut. Le langage n’a pas besoin d’exiger beaucoup de boilerplate pour cela.
    • Rust montre bien qu’une programmation orientée « railway » avec un support syntaxique de premier ordre peut presque éliminer le boilerplate. Personnellement, je trouve que le type/trait Result de Rust a vraiment réussi la gestion des erreurs.
    • La différence entre les erreurs comme valeurs et les exceptions est, à mon avis, surtout utile pour parler des erreurs propres au domaine. Les erreurs de domaine doivent être représentées comme des valeurs ; les problèmes sans lien avec le domaine, comme une base de données qui ne répond pas, sont plus naturellement des exceptions.
      Même pour un fichier ou un enregistrement absent, cela dépend de ce qui est attendu. Si la requête cherche quelque chose à partir d’un ID fourni par l’utilisateur, cette recherche fait elle-même partie de la validation de l’entrée utilisateur. Mais si l’on interroge un stockage avec un nom de blob enregistré dans un enregistrement de base de données et que le blob n’existe pas, c’est typiquement une bonne situation à traiter comme une exception.
      La frontière entre erreurs et exceptions est floue et varie selon les équipes et les domaines de problème, mais cela me semble être une règle empirique correcte.
    • En dehors des erreurs au niveau système, j’aimerais connaître des exemples d’états d’erreur qui ne soient pas des « erreurs attendues, concrètes et bien définies devant être traitées au point où elles se produisent ». Je voudrais comprendre si, dans ces cas-là, les exceptions sont meilleures à cause de la nature du problème ou à cause de contraintes du runtime.
      Dans du code C++, on peut vérifier les données au départ, faire crasher l’application si la vérification échoue, puis déboguer et corriger le code de vérification initial à partir de l’état d’erreur découvert.
      Les données que je manipule sont relativement bien définies, car elles suivent des formats de données CAD ou une topologie géométrique connus ; il est donc « assez facile » de gérer les conditions d’erreur, dans la mesure où l’on peut d’abord comprendre ce qu’est une donnée correcte.
  • Honnêtement, tout ce site est une mine d’or. Même si vous n’avez pas l’intention d’utiliser un langage fonctionnel, il offre un autre point de vue, et cela m’a beaucoup aidé personnellement. Je recommande aussi les autres articles.

    • La série qui construit des combinateurs de parseurs à partir de zéro est l’une des ressources les plus précieuses que j’aie lues et suivies. Beaucoup des concepts et mécanismes qu’on y trouve se sont révélés extrêmement utiles pour manipuler des langages typés dans un style fonctionnel.
      Cela dit, je crois que je ne saurai jamais vraiment ce qu’est une monade.
      https://fsharpforfunandprofit.com/series/understanding-parse...
  • Je me demande si l’affirmation « repousser la gestion des erreurs loin du point d’appel est une mauvaise chose » tient vraiment. Comme l’appelant est le mieux placé pour traiter l’erreur, j’aurais tendance à penser qu’il devrait la gérer directement plutôt que la transmettre plus bas.
    Par exemple, dans un flux validate and-then update-db and-then send-email, il peut être acceptable de renvoyer à l’appelant un échec de validate. Mais si update-db échoue, il faut décider s’il faut réessayer, utiliser un autre service, remettre dans une file, ou prévenir l’utilisateur. Même chose pour un échec de send-email.
    Au final, on se retrouve peut-être avec une structure de branchement plus complexe, du genre : notifier l’utilisateur pour ValidationError, réessayer dans 5 minutes ou notifier après une tentative distante pour DBError, remettre l’e-mail en file et considérer l’opération comme réussie pour EmailFailed. Peut-être que c’est acceptable.

    • La gestion d’erreurs façon monade permet de prendre le meilleur des deux mondes.
      Les gens qui n’aiment pas Go se plaignent de devoir vérifier les conditions d’erreur trop souvent, et les exceptions vérifiées de Java posent un problème similaire. Avec Either, on n’est pas forcé de vérifier : on peut le faire soi-même ou transmettre à l’appelant.
      Les gens qui n’aiment pas les exceptions non vérifiées de Java se plaignent de ne pas savoir ce qui peut être lancé ni quand. Avec Either, c’est explicite.
      Le point central, c’est qu’on ne sait pas quoi faire quand une erreur survient, et Either correspond bien à cette incertitude. Dans une base de code Java actuelle, si Limit getUserLimit(User) ne trouve rien, il peut lancer une exception NotFound, retourner null, ou retourner une valeur par défaut ; on ne le sait pas avant d’aller fouiller le code.
      Avec Either, l’appelant pourrait s’y fier sans lire le code interne, et le transformer facilement en une autre condition d’échec avec des outils intégrés comme orElse(null) ou orElseThrow(...). De plus, Either est une expression, donc il se prête bien à l’abstraction : on peut écrire une logique de reprise générique au lieu d’un code de retry spécialisé à l’intérieur de DbUpdater.
    • J’ai essayé d’implémenter cette logique dans le style try-catch de Java, et le résultat paraît bien pire, avec des try imbriqués, des catch(DBError), sleep(5), une tentative distante, une remise en file de EmailFailedError et une notification ValidationError qui s’entremêlent.
      https://gist.github.com/Andrewp2/9d97bd213b061166d6df565ce26...
      Quelqu’un pourrait sans doute écrire une meilleure version en try-catch, mais je la trouverais quand même nettement moins bonne que la version ferroviaire proposée.
    • La gestion des erreurs de bas niveau et de haut niveau peut coexister, mais je pense que ce style convient mieux aux exemples de tableau blanc ou aux situations où l’on construit quelque chose une fois pour toutes.
      J’aime le DDD, mais un programmeur ordinaire comme moi a besoin d’un cycle de maintenance raisonnable et maîtrisable. Ce qui rendait le TDD intéressant dans du code modifiable, c’est qu’il réduisait la charge cognitive de la maintenance.
      La programmation ferroviaire peut transporter beaucoup de contexte à travers de nombreuses transformations, et je veux éviter de me retrouver, en bout de ligne, avec un train chargé de tout un tas de contexte métier et système auquel on n’est pas préparé.
      Je pense que le DDD et l’architecture en oignon offrent le meilleur des deux mondes. Au final, il y a bien une voie ferrée, mais elle est à l’extérieur, pas incrustée dans une micro-application. Les problèmes de style ferroviaire devraient être explicites et codés de façon un peu laborieuse ; si c’est bien fait, la plupart des décisions ferroviaires sont des décisions métier qu’il ne faut pas contourner par une structure de code astucieuse.
      La programmation monadique est excellente, mais il y a une tentation de l’utiliser pour éviter des décisions métier nécessaires. Mettre l’ambiguïté dans le système de types peut mener plus tard à des situations difficiles, voire impossibles.
    • La gestion des erreurs de bas niveau et de haut niveau peut coexister. Ce n’est pas différent d’un grand try/catch contenant des try/catch spécialisés.
      Par exemple, à l’intérieur de la fonction send-email, on peut choisir de gérer une erreur transitoire très rapide. Si la connexion s’établit dans le délai autorisé, on revient sur le chemin de succès ; sinon, on renvoie l’erreur à l’appelant.
      S’il y a une règle claire, c’est que tout traitement nécessitant une intervention humaine doit impérativement être encapsulé et remonté. Même si l’on revient sur le chemin de succès, il vaut mieux laisser un log d’avertissement.
    • Cela peut se terminer comme dans votre exemple, mais on peut aussi le structurer différemment tout en conservant le même pipeline de haut niveau de base.
      notify-user peut se charger des notifications utilisateur propres à ValidationError et DBError, try-update-db peut regrouper update-db, une nouvelle tentative 5 minutes plus tard et une tentative distante, et try-send-email peut, en cas d’échec, remettre l’e-mail en file et transformer le résultat en succès.
      Le niveau supérieur reste alors quelque chose comme validate and-then try-update-db and-then try-send-email or-then notify-user. Il me semble que and/or/then a été correctement transposé vers map/bind.
  • Ce site est le meilleur site d’apprentissage de la programmation que j’aie rencontré pour enseigner des concepts concrets et pratiques. J’aime particulièrement l’idée de rendre les états invalides impossibles à représenter, et j’essaie de l’appliquer quel que soit le langage. Bien sûr, certains langages le facilitent davantage.
    https://fsharpforfunandprofit.com/posts/designing-with-types...

  • Utiliser un flux de données plutôt que des appels/retours résout mieux ce problème, et le fait en grande partie disparaître tout seul
    Dans le modèle appel/retour, il faut bien renvoyer quelque chose ; donc s’il n’y a pas de valeur à renvoyer à cause d’une erreur, il faut renvoyer une erreur, ou, comme en Go, renvoyer à la fois l’erreur et la valeur normale. Cela pollue le chemin de succès. En enchaînant le reste du traitement via un conteneur polymorphe, on améliore un peu les choses, mais le problème demeure
    Dans un flux de données, il suffit de ne rien envoyer à l’étape de filtrage suivante ; le chemin de succès n’est donc pas du tout affecté. Les erreurs peuvent être envoyées vers un endroit comme la sortie d’erreur standard, et centralisées au niveau de l’application
    Cela peut sembler trop beau pour être vrai, mais nous l’avons réellement utilisé chez Wunderlist et cela fonctionnait bien. La même technique employée quand il n’y a pas de valeur s’applique telle quelle quand la valeur n’est pas encore là, c’est-à-dire au traitement asynchrone

    • Je me demande si c’est fondamentalement différent de renvoyer des données ou de lancer une exception pour qu’elle soit attrapée ailleurs
    • Ce serait bien d’avoir un exemple de code pour aider à comprendre
    • Cette approche a aussi des inconvénients, et me semble notamment limitée. La gestion des exceptions, en particulier des exceptions asynchrones, peut rendre la récupération après erreur difficile
      Dans un flux de données simple, s’il y a une erreur, il n’y a pas de données pour l’étape suivante, donc l’arrêt est acceptable. Mais si sqrt(-1) « crashe » immédiatement et bascule vers une sortie d’erreur ou du logging, cela devient difficile, dans certains cas d’usage, lorsqu’on veut élargir le domaine pour fournir une autre valeur pour les nombres négatifs
      C’est là qu’un type d’erreur explicite comme Maybe est utile. On écrit le chemin de succès comme s’il n’y avait pas d’erreur, mais on utilise un autre opérateur pour composer les fonctions. Si l’on veut récupérer d’une erreur, on traite les deux chemins à cet endroit, et après cela on peut même ne plus autoriser les erreurs. Ce que j’apprécie particulièrement, c’est que pour extraire une vraie valeur d’une abstraction de type « valeur peut-être présente », il faut traiter élégamment les deux chemins
      Avec des types d’erreur explicites, on peut aussi sortir de la dichotomie succès/erreur. On peut exprimer des états comme « pas de valeur, mais… » ou « une valeur, mais… », et l’on peut aussi accumuler des erreurs issues de plusieurs tâches exécutables en parallèle pour les rassembler dans le résultat. On peut même aller plus loin et encoder du non-déterminisme, par exemple pour du parcours de graphe
      On peut aussi transporter l’état de façon pure, comme le font les programmeurs Haskell, et la célèbre monade IO de Haskell est moins une représentation du chemin réel qu’un dispositif limitant à main les points de contact avec le monde extérieur. C’est au service de la pureté, mais je ne pense pas que la pureté soit toujours une valeur à poursuivre de manière absolue
      Cette technique est bien plus variée qu’un simple chemin de fer, et fournit une façon propre de n’écrire que le chemin de succès tout en conservant la pureté. Elle est aussi avantageuse pour l’analyse statique fondée sur des équations et pour des tests unitaires sans mocks. J’aimerais que davantage de langages grand public fournissent mieux cette boîte à outils monadique
  • Liens liés : Railway Oriented Programming - https://news.ycombinator.com/item?id=31404643 - mai 2022, Railway-Oriented Programming (2015) - https://news.ycombinator.com/item?id=17337155 - juin 2018, Railway oriented programming - https://news.ycombinator.com/item?id=11955917 - juin 2016, Railway Oriented Programming - https://news.ycombinator.com/item?id=9166943 - mars 2015, Railway-oriented programming - https://news.ycombinator.com/item?id=7887134 - juin 2014
    En complément, What is railway oriented programming? (2020) - https://news.ycombinator.com/item?id=34245639 - janvier 2023, Railway Oriented Programming in Elixir (2015) - https://news.ycombinator.com/item?id=11958578 - juin 2016

  • Ce style est contagieux, comme les autres monades, et toute la logique métier finit par prendre cette forme. Si cela vous convient, vous pouvez continuer à lire
    Les exceptions sont, comme leur nom l’indique, des erreurs inattendues ; elles déroulent la pile jusqu’à l’endroit où elles sont attrapées, en effectuant automatiquement les nettoyages nécessaires. Elles conviennent aux erreurs qui se produisent hors du périmètre des fonctions métier, comme les erreurs d’I/O ou le manque de mémoire
    Si vous voulez valider des données, vous n’avez pas besoin de vous appuyer sur un gestionnaire d’exceptions ou sur une monade. Il suffit de faire du résultat du pipeline fonctionnel une donnée. S’il y a des erreurs de validation, rassemblez-les dans un ensemble, puis vérifiez-les une fois le pipeline terminé. En général, il y a de fortes chances que vous ne vouliez pas échouer dès la première erreur
    Si le pipeline a besoin d’effets de bord, ne les exécutez pas ; décrivez plutôt ces effets sous forme de données, puis exécutez-les plus tard. Si vous devez sortir tôt au milieu du traitement, coupez le pipeline à cet endroit, traitez le résultat, puis injectez-le dans un autre pipeline
    Ou bien vous pouvez utiliser une chaîne d’intercepteurs pour décider s’il faut poursuivre le chemin de succès. Si le langage le prend en charge, utilisez le pattern matching. L’important est que la décision de sauter ou non une partie du pipeline soit prise en dehors des fonctions métier ; celles-ci peuvent alors se concentrer uniquement sur de pures transformations de données, ce qui augmente leur réutilisabilité

  • Le simple fait que ce sujet revienne et soit discuté est une bonne chose. J’ai lu ce site pendant un an et suivi les exemples ; c’était l’une des meilleures ressources F#
    Les concepts abordés sur ce site m’ont aidé à penser de manière fonctionnelle dans tous les langages
    Le site est ancien, mais s’il suscite de l’intérêt aujourd’hui, je me demande si cela signifie que F# gagne en élan. Et je me demande aussi si les programmeurs d’autres langages utilisent un traitement des erreurs « ferroviaire » comme en Rust

    • J’ai un solide background C# pour les applications desktop, et avec le temps mon style de code C# devient de plus en plus fonctionnel. J’utilise beaucoup Linq, ainsi que des classes immuables et des types de retour capables de contenir des informations d’erreur détaillées
      Malgré tout, la raison pour laquelle je ne passe pas à F#, c’est que je passe la plupart de mon temps à ajuster des front-ends desktop. Dans ce domaine, F# ne semble pas apporter grand-chose. Les front-ends desktop prêts pour la production ne sont pas du F# natif, mais surtout du code orienté objet basé sur C#, relié à F# par une fine couche de glue code
      Dès qu’il faut personnaliser une UI un tant soit peu complexe, on doit revenir dans le monde orienté objet ; dans ce cas, autant le faire directement en C#. Personnellement, je pense que pour que F# gagne en traction, il lui faut un framework d’UI natif
    • On utilise aussi ce genre d’approche en C++. On définit un type de retour template paramétrable par le type de retour T et par une énumération spécifique définissant les conditions de résultat
      Si on est pressé, on peut aussi le faire avec std::pair, ou bien renvoyer une phrase d’erreur dans une chaîne et terminer tôt si la chaîne n’est pas vide. Si toutes les fonctions renvoient ce type, la phrase d’erreur peut remonter jusqu’au niveau supérieur
  • J’aime que cet article ressorte tous les quelques années. C’est vraiment un excellent article et une excellente présentation
    Ce qui est aussi intéressant, c’est qu’à chaque relecture je me trouve à un autre stade de mon parcours technique, en termes de compétences ou de philosophie, et qu’il me paraît donc différent à chaque fois

    • J’adore ce genre de ressources. Au début, j’ai regardé tous les six mois pendant quelques années une présentation Python intitulée “so you want to be a python expert”, et à chaque fois je comprenais davantage de choses. Même si je la revoyais aujourd’hui, j’en retirerais probablement encore quelque chose de nouveau