Comment puis-je construire un système qui présente toutes les caractéristiques suivantes :
- Utilisation de fonctions pures avec des objets immuables.
- Ne passez dans une fonction que les données dont elle a besoin, pas plus (c'est-à-dire pas de gros objet d'état d'application)
- Évitez d'avoir trop d'arguments pour les fonctions.
- Évitez d'avoir à construire de nouveaux objets uniquement dans le but de compresser et de décompresser les paramètres des fonctions, simplement pour éviter que trop de paramètres ne soient transmis aux fonctions. Si je vais regrouper plusieurs éléments dans une fonction comme un seul objet, je veux que cet objet soit le propriétaire de ces données, pas quelque chose construit temporairement
Il me semble que la monade d'État enfreint la règle n ° 2, bien que ce ne soit pas évident car elle est tissée à travers la monade.
J'ai le sentiment que je dois utiliser les lentilles d'une manière ou d'une autre, mais très peu est écrit à ce sujet pour les langages non fonctionnels.
Contexte
En tant qu'exercice, je convertis une de mes applications existantes d'un style orienté objet en un style fonctionnel. La première chose que j'essaie de faire est de tirer le maximum du noyau interne de l'application.
Une chose que j'ai entendue, c'est que comment gérer "State" dans un langage purement fonctionnel, et c'est ce que je crois que c'est fait par les monades d'État, c'est que logiquement, vous appelez une fonction pure, "en passant dans l'état de la monde tel qu'il est ", puis lorsque la fonction revient, elle vous renvoie l'état du monde tel qu'il a changé.
Pour illustrer, la façon dont vous pouvez faire un "monde bonjour" d'une manière purement fonctionnelle est un peu comme, vous passez dans votre programme cet état de l'écran, et recevez l'état de l'écran avec "bonjour le monde" imprimé dessus. Donc, techniquement, vous appelez une fonction pure et il n'y a pas d'effets secondaires.
Sur cette base, j'ai parcouru mon application et: 1. D'abord, j'ai mis tout mon état d'application dans un seul objet global (GameState) 2. Ensuite, j'ai rendu GameState immuable. Tu ne peux pas le changer. Si vous avez besoin d'un changement, vous devez en construire un nouveau. J'ai fait cela en ajoutant un constructeur de copie, qui prend éventuellement un ou plusieurs champs qui ont changé. 3. À chaque application, je passe le GameState en paramètre. Dans la fonction, une fois qu'il a fait ce qu'il va faire, il crée un nouveau GameState et le renvoie.
Comment j'ai un noyau fonctionnel pur et une boucle à l'extérieur qui alimente ce GameState dans la boucle de flux de travail principale de l'application.
Ma question:
Maintenant, mon problème est que le GameState a environ 15 objets immuables différents. La plupart des fonctions au niveau le plus bas ne fonctionnent que sur quelques-uns de ces objets, tels que le maintien du score. Donc, disons que j'ai une fonction qui calcule le score. Aujourd'hui, le GameState est passé à cette fonction, qui modifie le score en créant un nouveau GameState avec un nouveau score.
Quelque chose à ce sujet semble faux. La fonction n'a pas besoin de l'intégralité de GameState. Il a juste besoin de l'objet Score. Je l'ai donc mis à jour pour transmettre la partition et renvoyer uniquement la partition.
Cela semblait logique, alors je suis allé plus loin avec d'autres fonctions. Certaines fonctions nécessiteraient que je transmette 2, 3 ou 4 paramètres à partir du GameState, mais comme j'ai utilisé le modèle tout au long du noyau externe de l'application, je passe de plus en plus dans l'état de l'application. Par exemple, en haut de la boucle de workflow, j'appellerais une méthode, qui appellerait une méthode qui appellerait une méthode, etc., jusqu'à l'endroit où le score est calculé. Cela signifie que le score actuel est transmis à travers toutes ces couches simplement parce qu'une fonction tout en bas va calculer le score.
Alors maintenant, j'ai des fonctions avec parfois des dizaines de paramètres. Je pourrais mettre ces paramètres dans un objet pour réduire le nombre de paramètres, mais je voudrais que cette classe soit l'emplacement principal de l'état de l'application d'état, plutôt qu'un objet qui est simplement construit au moment de l'appel simplement pour éviter de passer dans plusieurs paramètres, puis décompressez-les.
Alors maintenant, je me demande si le problème que j'ai est que mes fonctions sont imbriquées trop profondément. C'est le résultat de vouloir avoir de petites fonctions, donc je refactorise quand une fonction devient trop grande et je la divise en plusieurs fonctions plus petites. Mais cela produit une hiérarchie plus profonde, et tout ce qui est transmis aux fonctions internes doit être transmis à la fonction externe même si la fonction externe ne fonctionne pas directement sur ces objets.
Il semblait que le simple passage du GameState évitait ce problème. Mais je reviens au problème d'origine de transmettre plus d'informations à une fonction que la fonction n'en a besoin.
la source
Réponses:
Je ne sais pas s'il y a une bonne solution. Cela peut être ou non une réponse, mais c'est beaucoup trop long pour un commentaire. Je faisais une chose similaire et les astuces suivantes ont aidé:
GameState
hiérarchiquement, vous obtenez donc 3 à 5 pièces plus petites au lieu de 15.Je ne pense pas. La refactorisation en petites fonctions est correcte, mais vous pourriez peut-être mieux les regrouper. Parfois, ce n'est pas possible, parfois il suffit d'un deuxième (ou troisième) examen du problème.
Comparez votre conception à celle modifiable. Y a-t-il des choses qui ont empiré avec la réécriture? Si oui, ne pouvez-vous pas les améliorer de la même manière que vous l'avez fait à l'origine?
la source
Je ne peux pas parler de C #, mais à Haskell, vous finiriez par passer tout l'État autour. Vous pouvez le faire explicitement ou avec une monade d'État. Une chose que vous pouvez faire pour résoudre le problème des fonctions recevant plus d'informations dont elles ont besoin est d'utiliser les classes de type Has. (Si vous n'êtes pas familier, les classes de types Haskell sont un peu comme les interfaces C #.) Pour chaque élément E de l'état, vous pouvez définir une classe de types HasE qui nécessite une fonction getE qui renvoie la valeur de E. La monade d'état peut alors être fait une instance de toutes ces classes de types. Ensuite, dans vos fonctions réelles, au lieu d'exiger explicitement votre monade d'état, vous avez besoin de toute monade appartenant aux classes de types A pour les éléments dont vous avez besoin; cela limite ce que la fonction peut faire avec la monade qu'elle utilise. Pour plus d'informations sur cette approche, voir Michael Snoyman'spublier sur le modèle de conception ReaderT .
Vous pourriez probablement reproduire quelque chose comme ça en C #, selon la façon dont vous définissez l'état qui est transmis. Si vous avez quelque chose comme
vous pouvez définir des interfaces
IHasMyInt
etIHasMyString
des méthodesGetMyInt
etGetMyString
respectivement. La classe d'état ressemble alors à:vos méthodes peuvent alors nécessiter IHasMyInt, IHasMyString ou l'ensemble de MyState, selon le cas.
Vous pouvez ensuite utiliser la contrainte where sur la définition de fonction afin de pouvoir passer l'objet d'état, mais il ne peut accéder qu'à string et int, et non à double.
la source
Je pense que vous feriez bien d'en apprendre davantage sur Redux ou Elm et comment ils traitent cette question.
Fondamentalement, vous avez une fonction pure qui prend tout l'état et l'action que l'utilisateur a effectuée et renvoie le nouvel état.
Cette fonction appelle ensuite d'autres fonctions pures, dont chacune gère une partie particulière de l'état. Selon l'action, bon nombre de ces fonctions peuvent ne faire que renvoyer l'état d'origine inchangé.
Pour en savoir plus, utilisez Google Elm Architecture ou Redux.js.org.
la source
Je pense que ce que vous essayez de faire est d'utiliser un langage orienté objet d'une manière qu'il ne devrait pas être utilisé, comme s'il était purement fonctionnel. Ce n'est pas comme si les langues OO étaient tout le mal. Il y a des avantages de l'une ou l'autre approche, c'est pourquoi nous pouvons maintenant mélanger le style OO avec le style fonctionnel et avoir la possibilité de rendre certains morceaux de code fonctionnels tandis que d'autres restent orientés objet afin que nous puissions profiter de toute la réutilisabilité, l'héritage ou le polimophisme. Heureusement, nous ne sommes plus tenus à l'une ou l'autre approche, alors pourquoi essayez-vous de vous limiter à l'un d'eux?
Répondre à votre question: non, je ne tisse aucun état particulier à travers la logique d'application mais j'utilise ce qui est approprié pour le cas d'utilisation actuel et j'applique les techniques disponibles de la manière la plus appropriée.
C # n'est pas (encore) prêt à être utilisé aussi fonctionnel que vous le souhaiteriez.
la source