Lors de la programmation en style fonctionnel, avez-vous un seul état d'application que vous tissez à travers la logique d'application?

12

Comment puis-je construire un système qui présente toutes les caractéristiques suivantes :

  1. Utilisation de fonctions pures avec des objets immuables.
  2. 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)
  3. Évitez d'avoir trop d'arguments pour les fonctions.
  4. É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.

Daisha Lynn
la source
1
Je ne suis pas un expert en design et spécialement fonctionnel, mais comme votre jeu par nature a un état qui évolue, êtes-vous sûr que la programmation fonctionnelle est un paradigme qui s'adapte à toutes les couches de votre application?
Walfrat
Walfrat, je pense que si vous parlez à des experts en programmation fonctionnelle, vous constaterez probablement qu'ils diraient que le paradigme de la programmation fonctionnelle a des solutions pour gérer l'état évolutif.
Daisha Lynn
Votre question m'a paru plus large et ne dit que cela. S'il ne s'agit que de gérer les états, voici un début: voir la réponse et le lien dans stackoverflow.com/questions/1020653/…
Walfrat
2
@DaishaLynn Je ne pense pas que vous devriez supprimer la question. Il a été voté positivement et personne n'essaye de le fermer, donc je ne pense pas que ce soit hors de portée pour ce site. Jusqu'à présent, l'absence de réponse est peut-être simplement due au fait qu'elle nécessite une expertise relativement spécifique. Mais cela ne signifie pas qu'il ne sera pas trouvé et ne sera finalement pas répondu.
Ben Aaronson
2
Gérer l'état mutable dans un programme fonctionnel pur et complexe sans assistance linguistique substantielle est une énorme douleur. Dans Haskell, c'est gérable à cause des monades, de la syntaxe laconique, de la très bonne inférence de type mais cela peut toujours être très ennuyeux. En C #, je pense que vous auriez beaucoup plus de mal.
Rétablir Monica le

Réponses:

2

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é:

  • Divisez le GameStatehiérarchiquement, vous obtenez donc 3 à 5 pièces plus petites au lieu de 15.
  • Laissez-le implémenter des interfaces, afin que vos méthodes ne voient que les parties nécessaires. Ne les jetez jamais en arrière car vous vous mentiriez au vrai type.
  • Laissez également les pièces implémenter des interfaces, afin que vous puissiez contrôler précisément ce que vous passez.
  • Utilisez des objets paramètres, mais faites-le avec parcimonie et essayez de les transformer en objets réels avec leur propre comportement.
  • Parfois, passer un peu plus que nécessaire est préférable à une longue liste de paramètres.

Alors maintenant, je me demande si le problème que j'ai est que mes fonctions sont imbriquées trop profondément.

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?

maaartinus
la source
Quelqu'un m'a dit de changer ma conception pour que la fonction ne prenne qu'un seul paramètre afin que je puisse utiliser le curry. J'ai essayé cette fonction, donc au lieu d'appeler DeleteEntity (a, b, c), maintenant j'appelle DeleteEntity (a) (b) (c). C'est donc mignon, et c'est censé rendre les choses plus composables, mais je ne comprends pas encore.
Daisha Lynn
@DaishaLynn J'utilise Java et il n'y a pas de sucre syntaxique doux pour le curry, donc (pour moi) ça ne vaut pas la peine d'essayer. Je suis plutôt sceptique quant à l'utilisation possible de fonctions d'ordre supérieur dans notre cas, mais faites-moi savoir si cela a fonctionné pour vous.
maaartinus
2

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

public class MyState
{
    public int MyInt {get; set; }
    public string MyString {get; set; }
}

vous pouvez définir des interfaces IHasMyIntet IHasMyStringdes méthodes GetMyIntet GetMyStringrespectivement. La classe d'état ressemble alors à:

public class MyState : IHasMyInt, IHasMyString
{
    public int MyInt {get; set; }
    public string MyString {get; set; }
    public double MyDouble {get; set; }

    public int GetMyInt () 
    {
        return MyInt;
    }

    public string GetMyString ()
    {
        return MyString;
    }

    public double GetMyDouble ()
    {
        return MyDouble;
    }
}

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.

public static T DoSomething<T>(T state) where T : IHasMyString, IHasMyInt
{
    var s = state.GetMyString();
    var i = state.GetMyInt();
    return state;
}
DylanSp
la source
C'est intéressant. Donc actuellement, là où je passe appelle une fonction et passe 10 paramètres par valeur, je passe 10 fois dans "gameSt'ate", mais à 10 types de paramètres différents, comme "IHasGameScore", "IHasGameBoard", etc. était un moyen de passer passer un seul paramètre que la fonction peut indiquer doit implémenter toutes les interfaces dans ce type. Je me demande si je peux le faire avec une "contrainte générique". Laissez-moi essayer.
Daisha Lynn
1
Ça a marché. Ici, cela fonctionne: dotnetfiddle.net/cfmDbs .
Daisha Lynn
1

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.

Daniel T.
la source
Je ne connais pas Elm, mais je pense que c'est similaire à Redux. Dans Redux, tous les réducteurs ne sont-ils pas appelés pour chaque changement d'état? Cela semble extrêmement inefficace.
Daisha Lynn
Lorsqu'il s'agit d'optimisations de bas niveau, ne présumez pas, mesurez. En pratique, c'est assez rapide.
Daniel
Merci Daniel, mais ça ne marchera pas pour moi. J'ai fait suffisamment de développement pour savoir qu'il ne fait pas notifier chaque composant de l'interface utilisateur à chaque fois que des données changent, peu importe si le contrôle se soucie du contrôle.
Daisha Lynn
-2

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.

t3chb0t
la source
3
Pas ravi de cette réponse, ni du ton. Je n'abuse de rien. Je repousse les limites de C # pour l'utiliser comme un langage plus fonctionnel. Ce n'est pas une chose rare à faire. Vous semblez y être philosophiquement opposé, ce qui est bien, mais dans ce cas, ne regardez pas cette question. Votre commentaire ne sert à personne. Passez.
Daisha Lynn
@DaishaLynn vous vous trompez, je ne m'y oppose en aucune façon et en fait je l'utilise beaucoup ... mais là où c'est naturel et possible et ne pas essayer de transformer un langage OO en un langage fonctionnel juste parce qu'il est branché faire cela. Vous n'êtes pas obligé d'être d'accord avec ma réponse, mais cela ne change pas le fait que vous n'utilisez pas correctement vos outils.
t3chb0t
Je ne le fais pas parce que c'est cool de le faire. C # lui-même s'oriente vers l'utilisation d'un style fonctionnel. Anders Hejlsberg lui-même l'a indiqué comme tel. Je comprends que vous êtes uniquement intéressé par l'utilisation de la langue dans le courant principal, et je comprends pourquoi et quand cela est approprié. Je ne sais juste pas pourquoi quelqu'un comme toi est même sur ce fil .. Comment aidez-vous vraiment?
Daisha Lynn
@DaishaLynn si vous ne pouvez pas faire face aux réponses critiquant votre question ou votre approche, vous ne devriez probablement pas poser ici de questions ou la prochaine fois, vous devriez simplement ajouter un avertissement disant que vous êtes uniquement intéressé par les réponses soutenant votre idée à 100% parce que vous ne le faites pas veulent entendre la vérité, mais plutôt obtenir des opinions favorables.
t3chb0t
Veuillez être un peu plus cordiaux les uns envers les autres. Il est possible de faire une critique sans dénigrer le langage. Tenter de programmer C # dans un style fonctionnel n'est certainement pas un «abus» ou un cas limite. C'est une technique courante utilisée par de nombreux développeurs C # pour apprendre d'autres langues.
zumalifeguard