Haben Sie beim Programmieren im funktionalen Stil einen einzelnen Anwendungszustand, den Sie durch die Anwendungslogik weben?

12

Wie erstelle ich ein System mit den folgenden Funktionen :

  1. Verwendung reiner Funktionen mit unveränderlichen Objekten.
  2. Übergeben Sie nur Daten an eine Funktion, die die Funktion benötigt, nicht mehr (dh kein großes Objekt für den Anwendungsstatus).
  3. Vermeiden Sie zu viele Argumente für Funktionen.
  4. Vermeiden Sie es, neue Objekte nur zum Packen und Entpacken von Parametern in Funktionen zu erstellen, um zu vermeiden, dass zu viele Parameter an Funktionen übergeben werden. Wenn ich mehrere Elemente in eine Funktion als ein einzelnes Objekt packen möchte, möchte ich, dass dieses Objekt Eigentümer dieser Daten ist und nicht vorübergehend erstellt wird

Mir scheint, die Staatsmonade verstößt gegen Regel Nr. 2, obwohl es nicht offensichtlich ist, weil sie durch die Monade eingewebt ist.

Ich habe das Gefühl, dass ich Objektive irgendwie benutzen muss, aber für nicht-funktionale Sprachen wird sehr wenig darüber geschrieben.

Hintergrund

Als Übung konvertiere ich eine meiner vorhandenen Anwendungen von einem objektorientierten Stil in einen funktionalen Stil. Das erste, was ich versuche, ist, so viel wie möglich aus dem inneren Kern der Anwendung herauszuholen.

Eine Sache, die ich gehört habe, ist, wie man "Staat" in einer rein funktionalen Sprache verwaltet, und ich glaube, dass dies von staatlichen Monaden getan wird, ist, dass man logischerweise eine reine Funktion nennt, "den Staat übergeben" world as it is ", wenn die Funktion zurückkehrt, kehrt sie zu Ihnen zurück, wie sich der Zustand der Welt geändert hat.

Um zu veranschaulichen, wie Sie eine "Hallo-Welt" auf rein funktionale Weise erstellen können, übergeben Sie in Ihrem Programm den Status des Bildschirms und erhalten den Status des Bildschirms mit der Aufschrift "Hallo-Welt" zurück. Technisch gesehen rufen Sie also eine reine Funktion auf, und es gibt keine Nebenwirkungen.

Darauf basierend habe ich meine Anwendung durchgesehen und: 1. Zuerst habe ich meinen gesamten Anwendungsstatus in ein einziges globales Objekt (GameState) gestellt. 2. Zweitens habe ich GameState unveränderlich gemacht. Du kannst es nicht ändern. Wenn Sie eine Änderung benötigen, müssen Sie eine neue erstellen. Ich habe dazu einen Copy-Konstruktor hinzugefügt, der optional ein oder mehrere Felder übernimmt, die sich geändert haben. 3. Zu jeder Anwendung übergebe ich den GameState als Parameter. Innerhalb der Funktion erstellt es, nachdem es getan hat, was es tun wird, einen neuen GameState und gibt ihn zurück.

Wie ich einen reinen Funktionskern und eine Schleife an der Außenseite habe, die diesen GameState in die Haupt-Workflow-Schleife der Anwendung einspeist.

Meine Frage:

Mein Problem ist nun, dass der GameState ungefähr 15 verschiedene unveränderliche Objekte hat. Viele der Funktionen auf der niedrigsten Ebene wirken sich nur auf wenige dieser Objekte aus, z. Nehmen wir also an, ich habe eine Funktion, die die Punktzahl berechnet. Heute wird der GameState an diese Funktion übergeben, die die Punktzahl ändert, indem ein neuer GameState mit einer neuen Punktzahl erstellt wird.

Daran scheint etwas nicht zu stimmen. Die Funktion benötigt nicht das gesamte GameState. Es wird nur das Score-Objekt benötigt. Also habe ich es aktualisiert, um die Partitur zu übergeben und nur die Partitur zurückzugeben.

Das schien Sinn zu machen, also ging ich mit anderen Funktionen weiter. Für einige Funktionen müsste ich 2, 3 oder 4 Parameter vom GameState übergeben, aber da ich das Muster bis zum äußeren Kern der Anwendung verwendet habe, übergebe ich immer mehr den Anwendungsstatus. Wie oben in der Workflow-Schleife würde ich eine Methode aufrufen, die eine Methode usw. aufruft, bis zu dem Punkt, an dem die Punktzahl berechnet wird. Das bedeutet, dass die aktuelle Punktzahl durch alle diese Ebenen geleitet wird, nur weil eine Funktion ganz unten die Punktzahl berechnet.

So, jetzt habe ich Funktionen mit manchmal Dutzenden von Parametern. Ich könnte diese Parameter in ein Objekt einfügen, um die Anzahl der Parameter zu verringern, aber dann möchte ich, dass diese Klasse der Master-Speicherort des Status der Statusanwendung ist und nicht ein Objekt, das zum Zeitpunkt des Aufrufs einfach erstellt wurde, um ein Übergeben zu vermeiden in mehreren Parametern, und packen Sie sie dann aus.

Jetzt frage ich mich, ob das Problem ist, dass meine Funktionen zu tief verschachtelt sind. Dies ist das Ergebnis des Wunsches nach kleinen Funktionen. Deshalb überarbeite ich, wenn eine Funktion zu groß wird, und teile sie in mehrere kleinere Funktionen auf. Dies führt jedoch zu einer tieferen Hierarchie, und alles, was in die inneren Funktionen übergeht, muss an die äußere Funktion übergeben werden, auch wenn die äußere Funktion diese Objekte nicht direkt bearbeitet.

Es schien, als würde dieses Problem vermieden, indem man den GameState einfach auf dem Weg dahin weitergab. Aber ich komme auf das ursprüngliche Problem zurück, mehr Informationen an eine Funktion zu übergeben, als die Funktion benötigt.

Daisha Lynn
quelle
1
Ich bin kein Experte für Design und besonders funktional, aber da Ihr Spiel von Natur aus einen sich entwickelnden Zustand aufweist, sind Sie sicher, dass funktionale Programmierung ein Paradigma ist, das in alle Ebenen Ihrer Anwendung passt?
Walfrat
Walfrat, ich denke, wenn Sie mit Experten für funktionale Programmierung sprechen, werden Sie wahrscheinlich feststellen, dass sie sagen würden, dass das Paradigma der funktionalen Programmierung Lösungen für das Management des sich entwickelnden Zustands bietet.
Daisha Lynn
Ihre Frage schien mir weiter zu gehen, als es nur heißt. Wenn es nur darum geht, Zustände zu verwalten, ist hier ein Anfang: Siehe die Antwort und den Link in stackoverflow.com/questions/1020653/…
Walfrat
2
@ DaishaLynn Ich glaube nicht, dass Sie die Frage löschen sollten. Es wurde hochgestuft und niemand versucht, es zu schließen, daher glaube ich nicht, dass es für diese Site nicht in Frage kommt. Das Fehlen einer Antwort liegt möglicherweise nur daran, dass dafür relativ viel Fachwissen erforderlich ist. Das heißt aber nicht, dass es irgendwann nicht mehr gefunden und beantwortet wird.
Ben Aaronson
2
Das Verwalten eines veränderlichen Zustands in einem komplexen reinen Funktionsprogramm ohne wesentliche Sprachunterstützung ist ein großer Schmerz. In Haskell ist es wegen Monaden, knapper Syntax und sehr guter Typinferenz handhabbar, aber es kann trotzdem sehr nervig sein. In C # hätten Sie vermutlich erheblich mehr Probleme.
Setzen Sie Monica

Antworten:

2

Ich bin mir nicht sicher, ob es eine gute Lösung gibt. Dies mag eine Antwort sein oder nicht, aber es ist viel zu lang für einen Kommentar. Ich habe etwas Ähnliches gemacht und die folgenden Tricks haben geholfen:

  • GameStateTeilen Sie die Hierarchie so auf, dass Sie 3-5 kleinere Teile anstelle von 15 erhalten.
  • Lassen Sie es Schnittstellen implementieren, damit Ihre Methoden nur die benötigten Teile sehen. Wirf sie niemals zurück, da du dich selbst über den wahren Typ belügen würdest.
  • Lassen Sie auch die Teile Schnittstellen implementieren, damit Sie die Kontrolle darüber haben, was Sie passieren.
  • Verwenden Sie Parameterobjekte, gehen Sie jedoch sparsam vor und versuchen Sie, sie in reale Objekte mit eigenem Verhalten umzuwandeln.
  • Manchmal ist es besser, etwas mehr als nötig zu übergeben als eine lange Parameterliste.

Jetzt frage ich mich, ob das Problem ist, dass meine Funktionen zu tief verschachtelt sind.

Ich glaube nicht. Das Umgestalten in kleine Funktionen ist richtig, aber vielleicht könnten Sie sie besser umgruppieren. Manchmal ist es nicht möglich, manchmal braucht es nur einen zweiten (oder dritten) Blick auf das Problem.

Vergleichen Sie Ihr Design mit dem veränderlichen. Gibt es Dinge, die sich durch das Umschreiben verschlimmert haben? Wenn ja, können Sie sie nicht auf dieselbe Weise verbessern, wie Sie es ursprünglich getan haben?

maaartinus
quelle
Jemand sagte mir, ich solle mein Design so ändern, dass für jede Funktion nur ein Parameter verwendet wird, damit ich das Currying verwenden kann. Ich habe diese eine Funktion ausprobiert. Anstatt DeleteEntity (a, b, c) aufzurufen, rufe ich jetzt DeleteEntity (a) (b) (c) auf. Das ist süß, und es soll die Dinge komponierbarer machen, aber ich verstehe es einfach noch nicht.
Daisha Lynn
@DaishaLynn Ich verwende Java und es gibt keinen süßen syntaktischen Zucker zum Curry, also (für mich) lohnt es sich nicht, es zu versuchen. Ich bin eher skeptisch in Bezug auf die mögliche Verwendung von Funktionen höherer Ordnung in unserem Fall, aber lassen Sie mich wissen, ob es für Sie funktioniert hat.
Maaartinus
2

Ich kann nicht mit C # sprechen, aber in Haskell würden Sie am Ende den ganzen Staat herumreichen. Sie können dies entweder explizit oder mit einer staatlichen Monade tun. Eine Sache, die Sie tun können, um das Problem zu lösen, dass Funktionen mehr Informationen erhalten, als sie benötigen, ist die Verwendung von Has-Typenklassen. (Wenn Sie nicht vertraut sind, ähneln Haskell-Typklassen C # -Schnittstellen.) Für jedes Element E des Status können Sie eine Typklasse HasE definieren, für die eine Funktion getE erforderlich ist, die den Wert E zurückgibt. Die Statusmonade kann dann sein machte eine Instanz von all diesen Typenklassen. Anstatt Ihre staatliche Monade explizit zu fordern, benötigen Sie in Ihren eigentlichen Funktionen jede Monade, die zu den Has-Typenklassen gehört, für die Elemente, die Sie benötigen. das schränkt ein, was die Funktion mit der Monade tun kann, die sie verwendet. Weitere Informationen zu diesem Ansatz finden Sie unter Michael Snoymanauf dem ReaderT-Entwurfsmuster posten .

Sie könnten wahrscheinlich so etwas in C # replizieren, je nachdem, wie Sie den Status definieren, der weitergegeben wird. Wenn Sie so etwas haben

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

Sie könnten Schnittstellen definieren IHasMyIntund IHasMyStringmit Methoden GetMyIntund GetMyStringjeweils. Die Staatsklasse sieht dann so aus:

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;
    }
}

In diesem Fall benötigen Ihre Methoden möglicherweise IHasMyInt, IHasMyString oder den gesamten MyState.

Sie können dann die where-Einschränkung für die Funktionsdefinition verwenden, damit Sie das Statusobjekt übergeben können, aber es kann nur zu string und int gelangen, nicht zu double.

public static T DoSomething<T>(T state) where T : IHasMyString, IHasMyInt
{
    var s = state.GetMyString();
    var i = state.GetMyInt();
    return state;
}
DylanSp
quelle
Das ist interessant. Derzeit übergebe ich, wenn ich eine Funktion übergebe und 10 Parameter als Wert übergebe, 10-mal den "gameSt'ate", aber 10 verschiedene Parametertypen, wie "IHasGameScore", "IHasGameBoard" usw., die ich mir dort wünsche war eine Möglichkeit, einen einzelnen Parameter zu übergeben, den die Funktion angeben kann, um alle Schnittstellen in diesem einen Typ zu implementieren. Ich frage mich, ob ich es mit einer "generischen Einschränkung" machen kann. Lassen Sie mich das versuchen.
Daisha Lynn
1
Es funktionierte. Hier funktioniert es: dotnetfiddle.net/cfmDbs .
Daisha Lynn
1

Ich denke, Sie sollten sich über Redux oder Elm und deren Umgang mit dieser Frage informieren.

Grundsätzlich haben Sie eine reine Funktion, die den gesamten Status und die vom Benutzer ausgeführte Aktion übernimmt und den neuen Status zurückgibt.

Diese Funktion ruft dann andere reine Funktionen auf, von denen jede einen bestimmten Teil des Zustands behandelt. Abhängig von der Aktion können viele dieser Funktionen nichts anderes tun, als den ursprünglichen Zustand unverändert wiederherzustellen.

Um mehr zu erfahren, Google the Elm Architecture oder Redux.js.org.

Daniel T.
quelle
Ich kenne Elm nicht, aber ich glaube, es ist Redux ähnlich. Werden in Redux nicht bei jedem Zustandswechsel alle Reduzierer gerufen? Klingt extrem ineffizient.
Daisha Lynn
Wenn es um Optimierungen auf niedriger Ebene geht, nehmen Sie nicht an, sondern messen Sie. In der Praxis ist es schnell genug.
Daniel T.
Danke Daniel, aber bei mir klappt es nicht. Ich habe genug entwickelt, um zu wissen, dass nicht jede Komponente in der Benutzeroberfläche benachrichtigt wird, wenn sich Daten ändern, unabhängig davon, ob sich die Steuerung um die Steuerung kümmert.
Daisha Lynn
-2

Ich denke, Sie versuchen, eine objektorientierte Sprache so zu verwenden, dass sie nicht so verwendet werden sollte, als ob sie rein funktional wäre. Es ist nicht so, dass OO-Sprachen das Böse waren. Es gibt Vorteile beider Ansätze, daher können wir jetzt den OO-Stil mit dem funktionalen Stil mischen und einige Codeteile funktionalisieren, während andere objektorientiert bleiben, so dass wir alle Vorteile der Wiederverwendbarkeit, Vererbung oder des Polimophismus nutzen können. Zum Glück sind wir nicht mehr an beide Ansätze gebunden. Warum versuchst du dich auf einen von ihnen zu beschränken?

Beantwortung Ihrer Frage: Nein, ich verwebe keine bestimmten Zustände durch die Anwendungslogik, sondern verwende das, was für den aktuellen Anwendungsfall angemessen ist, und wende die verfügbaren Techniken auf die am besten geeignete Weise an.

C # ist (noch) nicht so funktionsfähig, wie Sie es möchten.

t3chb0t
quelle
3
Weder von dieser Antwort noch vom Ton begeistert. Ich missbrauche nichts. Ich gehe an die Grenzen von C #, um es als funktionalere Sprache zu verwenden. Das ist keine Seltenheit. Sie scheinen philosophisch dagegen zu sein, was in Ordnung ist, aber schauen Sie in diesem Fall nicht auf diese Frage. Ihr Kommentar ist für niemanden von Nutzen. Mach weiter.
Daisha Lynn
@DaishaLynn du liegst falsch, ich bin in keiner Weise dagegen und tatsächlich benutze ich es oft ... aber wo es natürlich und möglich ist und nicht versucht, eine OO-Sprache in eine funktionale zu verwandeln, nur weil es hip ist um das zu tun. Sie müssen meiner Antwort nicht zustimmen, aber es ändert nichts an der Tatsache, dass Sie Ihre Werkzeuge nicht richtig verwenden.
t3chb0t
Ich mache das nicht, weil es hip ist, das zu tun. C # selbst verwendet einen funktionalen Stil. Anders Hejlsberg selbst hat dies als solche gekennzeichnet. Ich verstehe, dass Sie nur an der allgemeinen Verwendung der Sprache interessiert sind, und ich verstehe, warum und wann dies angemessen ist. Ich weiß nur nicht, warum jemand wie du überhaupt in diesem Thread ist. Wie hilfst du wirklich?
Daisha Lynn
@DaishaLynn Wenn Sie mit Antworten, die Ihre Frage oder Herangehensweise kritisieren, nicht zurechtkommen, sollten Sie hier wahrscheinlich keine Fragen stellen, oder wenn Sie das nächste Mal nur einen Haftungsausschluss hinzufügen, der besagt, dass Sie nur an Antworten interessiert sind, die Ihre Idee zu 100% unterstützen, weil Sie dies nicht tun Ich möchte die Wahrheit hören, sondern unterstützende Meinungen einholen.
t3chb0t
Seien Sie bitte ein bisschen herzlicher miteinander. Es ist möglich, Kritik zu üben, ohne die Sprache herabzusetzen. Der Versuch, C # in einem funktionalen Stil zu programmieren, ist mit Sicherheit kein "Missbrauch" oder ein Edge-Case. Diese Technik wird häufig von vielen C # -Entwicklern verwendet, um aus anderen Sprachen zu lernen.
Zumalifeguard