562 Stimmen

Groß angelegter Entwurf in Haskell?

Was ist ein guter Weg, um große funktionale Programme zu entwerfen/strukturieren, insbesondere in Haskell?

Ich habe mir einige der Tutorials angeschaut (Write Yourself a Scheme ist mein Favorit, Real World Haskell steht an zweiter Stelle) - aber die meisten Programme sind relativ klein und für einen einzigen Zweck gedacht. Außerdem halte ich einige von ihnen nicht für besonders elegant (zum Beispiel die riesigen Lookup-Tabellen in WYAS).

Ich möchte jetzt größere Programme mit mehr beweglichen Teilen schreiben - Daten aus einer Vielzahl verschiedener Quellen erfassen, sie bereinigen, sie auf verschiedene Weise verarbeiten, sie in Benutzeroberflächen anzeigen, sie aufbewahren, über Netzwerke kommunizieren usw. Wie könnte man einen solchen Code am besten strukturieren, damit er lesbar, wartbar und an veränderte Anforderungen anpassbar ist?

Es gibt eine recht umfangreiche Literatur, die sich mit diesen Fragen für große objektorientierte, imperative Programme befasst. Ideen wie MVC, Design Patterns usw. sind gute Rezepte für die Verwirklichung breiter Ziele wie die Trennung von Belangen und Wiederverwendbarkeit in einem OO-Stil. Darüber hinaus eignen sich neuere imperative Sprachen für einen "Design as you grow"-Stil des Refactorings, für den Haskell meiner Meinung nach weniger gut geeignet ist.

Gibt es eine entsprechende Literatur für Haskell? Wie lässt sich der Zoo an exotischen Kontrollstrukturen, die in der funktionalen Programmierung zur Verfügung stehen (Monaden, Pfeile, applicative usw.), am besten für diesen Zweck nutzen? Welche Best Practices können Sie empfehlen?

Danke!

EDIT (dies ist eine Folgemaßnahme zu Don Stewarts Antwort):

@dons erwähnt: "Monaden fangen wichtige architektonische Designs in Typen ein."

Ich schätze, meine Frage ist: Wie sollte man über wichtige architektonische Designs in einer rein funktionalen Sprache nachdenken?

Betrachten Sie das Beispiel mit mehreren Datenströmen und mehreren Verarbeitungsschritten. Ich kann modulare Parser für die Datenströme in eine Reihe von Datenstrukturen schreiben, und ich kann jeden Verarbeitungsschritt als reine Funktion implementieren. Welche Verarbeitungsschritte für ein bestimmtes Datenelement erforderlich sind, hängt von seinem Wert und dem der anderen ab. Auf einige der Schritte sollten Nebeneffekte wie GUI-Aktualisierungen oder Datenbankabfragen folgen.

Was ist der "richtige" Weg, um die Daten und die Parsing-Schritte in einer schönen Weise zu verbinden? Man könnte eine große Funktion schreiben, die das Richtige für die verschiedenen Datentypen tut. Oder man könnte eine Monade verwenden, um zu verfolgen, was bisher verarbeitet wurde, und jeden Verarbeitungsschritt das, was er als nächstes braucht, aus dem Zustand der Monade holen lassen. Oder man könnte weitgehend getrennte Programme schreiben und Nachrichten herumschicken (diese Option gefällt mir nicht besonders).

Die Folien, die er verlinkt hat, enthalten einen Aufzählungspunkt "Dinge, die wir brauchen": "Idiome für die Abbildung von Design auf Typen/Funktionen/Klassen/Monaden". Was sind die Idiome? :)

9 Stimmen

Ich denke, der Kerngedanke beim Schreiben großer Programme in einer funktionalen Sprache ist kleine, spezialisierte und zustandslose Module, die über Nachrichtenübermittlung kommunizieren . Natürlich muss man sich ein wenig verstellen, denn ein echtes Programm braucht einen Zustand. Ich denke, hier glänzt F# gegenüber Haskell.

18 Stimmen

@Chaos, aber nur Haskell erzwingt standardmäßig Zustandslosigkeit, Sie haben keine Wahl und müssen hart arbeiten, um in Haskell einen Zustand einzuführen (um die Kompositionalität zu brechen) :-)

0 Stimmen

@Don - Ja, ich weiß, aber ich bin einer dieser Typen, die das Beste aus beiden Welten haben.

515voto

Don Stewart Punkte 136046

Ich spreche ein wenig darüber in Entwicklung großer Projekte in Haskell und in der Entwurf und Implementierung von XMonad. Beim Engineering im Großen geht es um das Management von Komplexität. Die primären Code-Strukturierungsmechanismen in Haskell zur Beherrschung der Komplexität sind:

Das Typensystem

  • Verwendung des Typensystems zur Durchsetzung von Abstraktionen, um Interaktionen zu vereinfachen.
  • Erzwingen von Schlüsselinvarianten über Typen
    • (z. B., dass bestimmte Werte einem bestimmten Bereich nicht entkommen können)
    • Dieser bestimmte Code führt keine IO aus, berührt die Festplatte nicht
  • Sicherheit durchsetzen: geprüfte Ausnahmen (Maybe/Either), Vermischung von Konzepten vermeiden (Word, Int, Address)
  • Gute Datenstrukturen (wie z.B. Reißverschlüsse) können einige Klassen von Tests überflüssig machen, da sie z.B. Out-of-Bounds-Fehler statisch ausschließen.

Der Profiler

  • Legen Sie objektive Belege für die Häufigkeit und das Zeitprofil Ihres Programms vor.
  • Vor allem die Heap-Profilierung ist der beste Weg, um unnötigen Speicherverbrauch zu vermeiden.

Reinheit

  • Reduzieren Sie die Komplexität drastisch, indem Sie Zustände entfernen. Rein funktionaler Code skaliert, weil er kompositorisch ist. Alles, was Sie brauchen, ist der Typ, um zu bestimmen, wie ein Code verwendet werden soll - er wird nicht auf mysteriöse Weise kaputt gehen, wenn Sie einen anderen Teil des Programms ändern.
  • Verwenden Sie viel "Model/View/Controller"-Programmierung: Parsen Sie externe Daten so schnell wie möglich in rein funktionale Datenstrukturen, bearbeiten Sie diese Strukturen, und wenn alle Arbeit getan ist, rendern/flushen/serialisieren Sie sie. Hält den größten Teil Ihres Codes rein

Prüfung

  • QuickCheck + Haskell Code Coverage, um sicherzustellen, dass Sie die Dinge testen, die Sie nicht mit Typen überprüfen können.
  • GHC + RTS ist großartig, um zu sehen, ob man zu viel Zeit mit GC verbringt.
  • QuickCheck kann Ihnen auch helfen, saubere, orthogonale APIs für Ihre Module zu identifizieren. Wenn die Eigenschaften Ihres Codes schwer zu beschreiben sind, sind sie wahrscheinlich zu komplex. Führen Sie so lange Refactoring durch, bis Sie einen sauberen Satz von Eigenschaften haben, mit denen Sie Ihren Code testen können und die sich gut zusammensetzen lassen. Dann ist der Code wahrscheinlich auch gut konzipiert.

Monaden zur Strukturierung

  • Monaden erfassen wichtige architektonische Entwürfe in Typen (dieser Code greift auf Hardware zu, dieser Code ist eine Einzelbenutzersitzung usw.)
  • Die Monade X in xmonad z. B. erfasst genau den Entwurf dafür, welcher Zustand für welche Komponenten des Systems sichtbar ist.

Typklassen und existentielle Typen

  • Typklassen zur Abstraktion verwenden: Implementierungen hinter polymorphen Schnittstellen verstecken.

Gleichzeitigkeit und Parallelität

  • Sneak par in Ihr Programm einbauen, um die Konkurrenz mit einfacher, zusammensetzbarer Parallelität zu schlagen.

Refactor

  • Sie können in Haskell refaktorisieren viel . Die Typen sorgen dafür, dass Ihre Änderungen in großem Umfang sicher sind, wenn Sie Typen sinnvoll einsetzen. Dies wird die Skalierung Ihrer Codebasis erleichtern. Stellen Sie sicher, dass Ihre Refactorings bis zur Fertigstellung Typfehler verursachen.

Das FFI sinnvoll nutzen

  • Das FFI macht es einfacher, mit fremdem Code zu spielen, aber dieser fremde Code kann gefährlich sein.
  • Seien Sie sehr vorsichtig bei Annahmen über die Form der zurückgegebenen Daten.

Meta-Programmierung

  • Ein wenig Template Haskell oder Generics können Boilerplate entfernen.

Verpackung und Vertrieb

  • Verwenden Sie Cabal. Entwickeln Sie nicht Ihr eigenes Build-System. (EDIT: Eigentlich sollten Sie wahrscheinlich die Stapel Jetzt geht's los.).
  • Verwenden Sie Haddock für gute API-Dokumente
  • Tools wie graphmod kann Ihre Modulstrukturen zeigen.
  • Verlassen Sie sich auf die Haskell-Plattform-Versionen von Bibliotheken und Werkzeugen, wenn dies möglich ist. Es ist eine stabile Basis. (EDIT: Auch hier gilt, dass Sie heutzutage wahrscheinlich die Stapel um eine stabile Basis zu schaffen).

Warnungen

  • Utilisez -Wall um Ihren Code frei von Gerüchen zu halten. Für mehr Sicherheit können Sie sich auch Agda, Isabelle oder Catch ansehen. Für eine lint-ähnliche Überprüfung, siehe die großartige hlint die Verbesserungen vorschlagen wird.

Mit all diesen Werkzeugen können Sie die Komplexität in den Griff bekommen, indem Sie so viele Wechselwirkungen zwischen den Komponenten wie möglich entfernen. Im Idealfall haben Sie eine sehr große Basis an reinem Code, der wirklich einfach zu pflegen ist, da er kompositorisch ist. Das ist nicht immer möglich, aber es ist erstrebenswert.

Generell: zersetzen die logischen Einheiten Ihres Systems in die kleinstmöglichen referenziell transparenten Komponenten zu zerlegen und diese dann in Modulen zu implementieren. Globale oder lokale Umgebungen für Gruppen von Komponenten (oder innerhalb von Komponenten) können auf Monaden abgebildet werden. Verwenden Sie algebraische Datentypen, um Kerndatenstrukturen zu beschreiben. Teilen Sie diese Definitionen weithin.

8 Stimmen

Danke, Don, deine Antwort ist ausgezeichnet - das sind alles wertvolle Leitlinien, und ich werde sie regelmäßig zu Rate ziehen. Ich schätze, meine Frage stellt sich einen Schritt früher, bevor man all dies braucht. Was ich wirklich gerne wissen würde, sind die "Idiome für die Abbildung von Design auf Typen/Funktionen/Klassen/Monaden" ... Ich könnte versuchen, meine eigenen zu erfinden, aber ich hatte gehofft, dass es irgendwo eine Reihe von Best Practices gibt - oder wenn nicht, Empfehlungen für gut strukturierten Code, den man in einem größeren System lesen kann (im Gegensatz zu, sagen wir, einer konzentrierten Bibliothek). Ich habe meinen Beitrag bearbeitet, um dieselbe Frage direkter zu stellen.

6 Stimmen

Ich habe etwas Text über die Zerlegung des Entwurfs in Module hinzugefügt. Ihr Ziel ist es, logisch zusammenhängende Funktionen in Modulen zu identifizieren, die referenziell transparente Schnittstellen mit anderen Teilen des Systems haben, und rein funktionale Datentypen so schnell wie möglich zu verwenden, so viel wie möglich, um die Außenwelt sicher zu modellieren. Das xmonad-Designdokument deckt vieles davon ab: xmonad.wordpress.com/2009/09/09/

0 Stimmen

Nochmals vielen Dank! Das xmonad Design Dokument ist genau das, wonach ich gesucht habe. Zeit, etwas Code zu lesen...

116voto

user349653 Punkte 1321

Don hat Ihnen die meisten Details oben genannt, aber hier sind meine zwei Cents, die ich bei der Erstellung von wirklich anspruchsvollen zustandsbehafteten Programmen wie Systemdämonen in Haskell gesammelt habe.

  1. Letztendlich leben Sie in einem Monaden-Transformator-Stapel. Ganz unten ist IO. Darüber bildet jedes wichtige Modul (im abstrakten Sinne, nicht im Sinne eines Moduls in einer Datei) seinen notwendigen Zustand auf eine Schicht in diesem Stapel ab. Wenn Sie also Ihren Datenbankverbindungscode in einem Modul versteckt haben, schreiben Sie alles über einen Typ MonadReader Connection m => ... -> m ... und dann können Ihre Datenbankfunktionen immer ihre Verbindung erhalten, ohne dass Funktionen aus anderen Modulen von deren Existenz wissen müssen. Am Ende könnte eine Schicht die Datenbankverbindung enthalten, eine andere die Konfiguration, eine dritte die verschiedenen Semaphoren und mvars für die Auflösung von Parallelität und Synchronisation, eine weitere die Handles der Protokolldateien usw.

  2. Überlegen Sie sich Ihre Fehlerbehandlung erste . Die größte Schwäche von Haskell in größeren Systemen ist derzeit die Fülle an Methoden zur Fehlerbehandlung, darunter auch so lausige wie Maybe (was falsch ist, weil man keine Informationen darüber zurückgeben kann, was falsch gelaufen ist; verwenden Sie immer Either statt Maybe, es sei denn, Sie meinen wirklich nur fehlende Werte). Überlegen Sie sich zuerst, wie Sie es machen wollen, und bauen Sie Adapter von den verschiedenen Fehlerbehandlungsmechanismen, die Ihre Bibliotheken und anderer Code verwenden, in Ihren endgültigen ein. Das wird Ihnen später viel Ärger ersparen.

Nachtrag (entnommen aus Kommentaren; Dank an Lii & liminalisht ) -
weitere Diskussionen über verschiedene Möglichkeiten, ein großes Programm in Monaden auf einem Stapel zu zerlegen:

Ben Kolera gibt eine hervorragende praktische Einführung in dieses Thema, und Brian Hurt erörtert Lösungen für das Problem der lift monadische Aktionen in Ihre benutzerdefinierte Monade zu integrieren. George Wilson zeigt, wie man mtl um Code zu schreiben, der mit jeder Monade funktioniert, die die erforderlichen Typklassen implementiert, und nicht mit Ihrer eigenen Monadenart. Carlo Hamalainen hat einige kurze, nützliche Notizen geschrieben, die Georges Vortrag zusammenfassen.

5 Stimmen

Zwei gute Punkte! Diese Antwort hat den Vorzug, dass sie einigermaßen konkret ist, was bei den anderen nicht der Fall ist. Es wäre interessant, mehr Diskussionen über verschiedene Möglichkeiten zu lesen, ein großes Programm in Monaden in einem Stapel aufzuteilen. Bitte posten Sie Links zu solchen Artikeln, wenn Sie welche haben!

6 Stimmen

@Lii Ben Kolera gibt eine hervorragende praktische Einführung in dieses Thema, und Brian Hurt erörtert Lösungen für das Problem der lift monadische Aktionen in Ihre benutzerdefinierte Monade zu integrieren. George Wilson zeigt, wie man mtl um Code zu schreiben, der mit jeder Monade funktioniert, die die erforderlichen Typklassen implementiert, und nicht mit Ihrer eigenen Monadenart. Carlo Hamalainen hat einige kurze, nützliche Notizen geschrieben, die Georges Vortrag zusammenfassen.

0 Stimmen

Ich stimme zu, dass Monaden-Transformator-Stapel dazu neigen, wichtige architektonische Grundlagen zu sein, aber ich versuche sehr hart, IO aus ihnen herauszuhalten. Es ist nicht immer möglich, aber wenn Sie darüber nachdenken, was "und dann" in Ihrer Monade bedeutet, könnten Sie entdecken, dass Sie wirklich eine Fortsetzung oder einen Automaten irgendwo am unteren Ende haben, der dann von einer "run"-Funktion in IO interpretiert werden kann.

43voto

augustss Punkte 668

Der Entwurf großer Programme in Haskell unterscheidet sich nicht wesentlich von dem in anderen Sprachen. Bei der Programmierung großer Programme geht es darum, das Problem in überschaubare Teile zu zerlegen und diese zusammenzufügen; die Implementierungssprache ist weniger wichtig.

Bei einem großen Entwurf ist es jedoch sinnvoll, das Schriftsystem zu nutzen, um sicherzustellen, dass die Teile nur korrekt zusammengefügt werden können. Dies könnte newtype oder Phantomtypen beinhalten, um Dinge, die scheinbar den gleichen Typ haben, unterschiedlich zu machen.

Wenn es darum geht, den Code im Laufe der Zeit zu überarbeiten, ist Reinheit ein großer Segen, also versuchen Sie, so viel Code wie möglich rein zu halten. Reiner Code ist einfach zu refaktorisieren, da er keine versteckten Interaktionen mit anderen Teilen des Programms hat.

14 Stimmen

Ich habe festgestellt, dass das Refactoring ziemlich frustrierend ist, wenn sich die Datentypen ändern müssen. Es erfordert eine mühsame Änderung der Arität vieler Konstruktoren und Pattern-Matches. (Ich stimme zu, dass das Refactoring reiner Funktionen in andere reine Funktionen desselben Typs einfach ist - solange man die Datentypen nicht berührt).

2 Stimmen

@Dan Sie können mit kleineren Änderungen (z. B. Hinzufügen eines Feldes) völlig frei umgehen, wenn Sie Datensätze verwenden. Manche wollen sich die Datensätze zur Gewohnheit machen (ich gehöre dazu ^^").

5 Stimmen

@Dan Ich meine, wenn man den Datentyp einer Funktion in einer beliebigen Sprache ändert, muss man dann nicht dasselbe tun? Ich wüsste nicht, wie eine Sprache wie Java oder C++ Ihnen in dieser Hinsicht helfen könnte. Wenn du sagst, dass du eine Art gemeinsame Schnittstelle verwenden kannst, der beide Typen gehorchen, dann hättest du das mit Typeclasses in Haskell tun sollen.

16voto

comonad Punkte 4988

Ich habe gelernt strukturiert funktionale Programmierung zum ersten Mal mit dieses Buch . Es ist vielleicht nicht genau das, was Sie suchen, aber für Anfänger in der funktionalen Programmierung kann dies einer der besten ersten Schritte sein, um zu lernen, wie man funktionale Programme strukturiert - unabhängig vom Maßstab. Auf allen Abstraktionsebenen sollte der Entwurf immer übersichtliche Strukturen aufweisen.

Die Kunst der funktionalen Programmierung

The Craft of Functional Programming

http://www.cs.kent.ac.uk/people/staff/sjt/craft2e/

11 Stimmen

So großartig das Craft of FP auch ist - ich habe Haskell von ihm gelernt - es ist ein Einleitungstext für Programmieranfänger nicht für den Entwurf von großen Systemen in Haskell.

3 Stimmen

Es ist das beste Buch, das ich über den Entwurf von APIs und das Verbergen von Implementierungsdetails kenne. Mit diesem Buch bin ich ein besserer Programmierer in C++ geworden - einfach weil ich gelernt habe, meinen Code besser zu organisieren. Nun, Ihre Erfahrung (und Antwort) ist sicherlich besser als dieses Buch, aber Dan ist wahrscheinlich immer noch ein Anfänger in Haskell. ( where beginner=do write $ tutorials `about` Monads )

11voto

Alexander Granin Punkte 953

Ich schreibe gerade an einem Buch mit dem Titel "Functional Design and Architecture". Es bietet Ihnen einen vollständigen Satz von Techniken, wie Sie eine große Anwendung mit einem rein funktionalen Ansatz erstellen können. Es beschreibt viele funktionale Muster und Ideen beim Aufbau einer SCADA-ähnlichen Anwendung "Andromeda" zur Steuerung von Raumschiffen von Grund auf. Meine Hauptsprache ist Haskell. Das Buch behandelt:

  • Ansätze zur Architekturmodellierung mit Diagrammen;
  • Analyse der Anforderungen;
  • Modellierung einer eingebetteten DSL-Domäne;
  • Externer DSL-Entwurf und Implementierung;
  • Monaden als Subsysteme mit Wirkungen;
  • Freie Monaden als funktionale Schnittstellen;
  • Gepfeilte eDSLs;
  • Inversion der Kontrolle durch freie monadische eDSLs;
  • Software-Transaktionsspeicher;
  • Linsen;
  • Staat, Leser, Schreiber, RWS, ST-Monaden;
  • Unreiner Zustand: IORef, MVar, STM;
  • Multithreading und gleichzeitige Domänenmodellierung;
  • GUI;
  • Anwendbarkeit der gängigen Techniken und Ansätze wie UML, SOLID, GRASP;
  • Interaktion mit verunreinigten Subsystemen.

Sie können sich mit dem Code für das Buch vertraut machen aquí und die Andromeda Projektcode.

Ich gehe davon aus, dass ich dieses Buch Ende 2017 fertigstellen werde. Bis dahin können Sie meinen Artikel "Design und Architektur in der funktionalen Programmierung" lesen (Rus) aquí .

UPDATE

Ich habe mein Buch online gestellt (die ersten 5 Kapitel). Siehe Beitrag auf Reddit

0 Stimmen

Alexander, könnten Sie diese Notiz bitte aktualisieren, wenn Ihr Buch fertig ist, damit wir es verfolgen können. Prost.

4 Stimmen

Klar! Im Moment habe ich die Hälfte des Textes fertig, aber das ist nur 1/3 der gesamten Arbeit. Also, behaltet euer Interesse, das inspiriert mich sehr!

2 Stimmen

Hallo! Ich habe mein Buch online gestellt (nur die ersten 5 Kapitel). Siehe Beitrag auf Reddit: reddit.com/r/haskell/comments/6ck72h/

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X