wake-up-neo.com

Großflächiges Design in Haskell?

Was ist eine gute Möglichkeit, große Funktionsprogramme zu entwerfen/strukturieren, insbesondere in Haskell?

Ich habe eine Reihe von Tutorials durchlaufen (Write Yourself a Scheme ist mein Favorit, Real World Haskell steht an zweiter Stelle) - aber die meisten Programme sind relativ klein und nur für einen Zweck gedacht. Darüber hinaus halte ich einige von ihnen nicht für besonders elegant (zum Beispiel die umfangreichen Nachschlagetabellen in WYAS).

Ich möchte jetzt größere Programme mit beweglicheren Teilen schreiben - Daten aus verschiedenen Quellen erfassen, bereinigen, auf verschiedene Weise verarbeiten, auf Benutzeroberflächen anzeigen, beibehalten, über Netzwerke kommunizieren usw. Wie könnte das sein? Eine der besten Strukturen für einen solchen Code ist, dass er lesbar, wartbar und an veränderte Anforderungen anpassbar ist.

Es gibt eine ziemlich große Literatur, die sich mit diesen Fragen für große objektorientierte Imperativprogramme befasst. Ideen wie MVC, Entwurfsmuster usw. sind anständige Vorgaben zur Verwirklichung allgemeiner Ziele wie Trennung von Bedenken und Wiederverwendbarkeit in einem OO -Stil. Darüber hinaus eignen sich neuere imperative Sprachen für einen Refactoring-Stil, für den Haskell meiner Meinung nach weniger geeignet ist.

Gibt es eine gleichwertige Literatur für Haskell? Wie wird der Zoo der exotischen Kontrollstrukturen in der funktionalen Programmierung (Monaden, Pfeile, Anwendungen usw.) am besten für diesen Zweck eingesetzt? Welche Best Practices können Sie empfehlen?

Vielen Dank!

BEARBEITEN (dies ist eine Folge der Antwort von Don Stewart):

@dons erwähnt: "Monaden erfassen wichtige architektonische Entwürfe in Typen."

Ich denke, meine Frage ist: Wie sollte man über wichtige architektonische Entwürfe in einer rein funktionalen Sprache denken?

Betrachten Sie das Beispiel mehrerer Datenströme und mehrerer Verarbeitungsschritte. Ich kann modulare Parser für die Datenströme in eine Reihe von Datenstrukturen schreiben und jeden Verarbeitungsschritt als reine Funktion implementieren. Die für ein Datenelement erforderlichen Verarbeitungsschritte hängen von seinem Wert und anderen ab. Einige der Schritte sollten von Nebenwirkungen wie GUI-Updates oder Datenbankabfragen gefolgt werden.

Was ist der richtige Weg, um die Daten und die Analyseschritte auf nette Weise zu verknüpfen? Man könnte eine große Funktion schreiben, die für die verschiedenen Datentypen das Richtige tut. Oder man kann eine Monade verwenden, um zu verfolgen, was bisher verarbeitet wurde, und jeden Verarbeitungsschritt aus dem Monadenzustand abrufen, was er als Nächstes benötigt. Oder man könnte größtenteils separate Programme schreiben und Nachrichten verschicken (mir gefällt diese Option nicht besonders).

Die von ihm verknüpften Folien enthalten die Aufschrift "Things we Need" (Dinge, die wir brauchen): "Redewendungen für die Zuordnung von Design zu Typen/Funktionen/Klassen/Monaden". Was sind die Redewendungen? :)

566
Dan

Ich spreche ein wenig darüber in Engineering großer Projekte in Haskell und in Design und Implementierung von XMonad Engineering im Großen bedeutet, Komplexität zu managen. Die primären Code-Strukturierungsmechanismen in Haskell zum Verwalten der Komplexität sind:

Das Typensystem

  • Verwenden Sie das Typsystem, um Abstraktionen zu erzwingen und Interaktionen zu vereinfachen.
  • Erzwinge Schlüsselinvarianten über Typen
    • (z. B., dass bestimmte Werte einem bestimmten Bereich nicht entgehen können)
    • Dieser bestimmte Code macht keine IO, berührt nicht die Festplatte
  • Sicherheit erzwingen: geprüfte Ausnahmen (Vielleicht/Entweder), Vermeiden von Mischkonzepten (Word, Int, Address)
  • Gute Datenstrukturen (wie Reißverschlüsse) können einige Testklassen überflüssig machen, da sie z.B. außerhalb der Grenzen Fehler statisch.

Der Profiler

  • Bieten Sie einen objektiven Nachweis für die Heap- und Zeitprofile Ihres Programms.
  • Insbesondere die Erstellung von Heap-Profilen ist die beste Methode, um unnötigen Speicherbedarf zu vermeiden.

Reinheit

  • Reduzieren Sie die Komplexität drastisch, indem Sie den Status 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 "Modell/Ansicht/Steuerung" -Programmierung: Analysieren Sie externe Daten so schnell wie möglich in rein funktionale Datenstrukturen, bearbeiten Sie diese Strukturen, und rendern/spülen/serialisieren Sie sie nach Abschluss aller Arbeiten. Hält den größten Teil Ihres Codes rein

Testen

  • QuickCheck + Haskell Code Coverage, um sicherzustellen, dass Sie die Dinge testen, die Sie nicht mit Typen überprüfen können.
  • Mit GHC + RTS können Sie feststellen, ob Sie zu viel Zeit mit GC verbringen.
  • Mit QuickCheck können Sie auch saubere, orthogonale APIs für Ihre Module identifizieren. Wenn sich die Eigenschaften Ihres Codes nur schwer angeben lassen, sind sie wahrscheinlich zu komplex. Führen Sie die Umgestaltung so lange 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 wohl auch gut gestaltet.

Monaden zur Strukturierung

  • Monaden erfassen wichtige Architekturentwürfe in Typen (dieser Code greift auf Hardware zu, dieser Code ist eine Einzelbenutzersitzung usw.).
  • Z.B. Die X-Monade in XMonad erfasst genau das Design für welchen Status für welche Komponenten des Systems sichtbar ist.

Typklassen und existenzielle Typen

  • Verwenden Sie Typklassen, um Abstraktion bereitzustellen: Verbergen Sie Implementierungen hinter polymorphen Schnittstellen.

Parallelität und Parallelität

  • Schleichen Sie par in Ihr Programm ein, um die Konkurrenz mit einfacher, komponierbarer Parallelität zu schlagen.

Refactor

  • Sie können in Haskell viel umgestalten . Die Typen stellen sicher, dass Ihre umfangreichen Änderungen sicher sind, wenn Sie die Typen mit Bedacht verwenden. Dies hilft Ihrer Codebasis-Skalierung. Stellen Sie sicher, dass Ihre Refactorings bis zum Abschluss Tippfehler verursachen.

Verwenden Sie den FFI mit Bedacht

  • Der FFI erleichtert das Spielen mit Fremdcode, dieser Fremdcode kann jedoch gefährlich sein.
  • Gehen Sie bei den Annahmen zur Form der zurückgegebenen Daten sehr vorsichtig vor.

Meta-Programmierung

  • Ein bisschen Template Haskell oder Generika können die Heizplatte entfernen.

Verpackung und Vertrieb

  • Benutze Cabal. Rollen Sie nicht Ihr eigenes Build-System. (BEARBEITEN: Eigentlich möchten Sie Stack jetzt verwenden, um loszulegen.)
  • Verwenden Sie Haddock für gute API-Dokumente
  • Werkzeuge wie graphmod können Ihre Modulstrukturen anzeigen.
  • Verlassen Sie sich, wenn möglich, auf die Haskell Platform-Versionen von Bibliotheken und Tools. Es ist eine stabile Basis. (BEARBEITEN: Heutzutage möchten Sie wahrscheinlich Stack verwenden, um eine stabile Basis zum Laufen zu bringen.)

Warnungen

  • Verwenden -Wall um Ihren Code frei von Gerüchen zu halten. Sie könnten auch Agda, Isabelle oder Catch für mehr Sicherheit anschauen. Informationen zur fusselfreien Prüfung finden Sie im großen hlint , das Verbesserungen vorschlägt.

Mit all diesen Tools können Sie die Komplexität im Griff behalten und so viele Interaktionen zwischen Komponenten wie möglich entfernen. Idealerweise verfügen Sie über eine sehr große Basis an reinem Code, der sehr einfach zu warten ist, da er kompositorisch ist. Das ist nicht immer möglich, aber es lohnt sich anzustreben.

Im Allgemeinen gilt: Zerlegen Sie die logischen Einheiten Ihres Systems in die kleinstmöglichen referenziell transparenten Komponenten und implementieren Sie sie dann in Module. Globale oder lokale Umgebungen für Komponentensätze (oder innerhalb von Komponenten) können Monaden zugeordnet werden. Verwenden Sie algebraische Datentypen, um Kerndatenstrukturen zu beschreiben. Teilen Sie diese Definitionen weit.

519
Don Stewart

Don gab Ihnen die meisten der oben genannten Details, aber hier sind meine zwei Cents, die ich von der Ausführung wirklich komplizierter Stateful-Programme wie System-Daemons in Haskell habe.

  1. Am Ende leben Sie in einem Monadentransformatorstapel. Unten ist IO. Darüber hinaus ordnet jedes Hauptmodul (im abstrakten Sinne, nicht im Sinne von Modul in einer Datei) seinen erforderlichen Status einer Ebene in diesem Stapel zu. Wenn Sie also Ihren Datenbankverbindungscode in einem Modul versteckt haben, schreiben Sie alles so, dass es über einen MonadReader-Verbindungstyp m => ... -> m ... erfolgt, und dann können Ihre Datenbankfunktionen ihre Verbindung immer ohne Funktionen von anderen erhalten Module müssen sich ihrer Existenz bewusst sein. Es kann vorkommen, dass eine Ebene Ihre Datenbankverbindung enthält, eine andere Ihre Konfiguration, eine dritte Ihre verschiedenen Semaphoren und MVARs für die Auflösung von Parallelität und Synchronisation, eine andere Ihre Protokolldatei-Handles usw.

  2. Finden Sie Ihre Fehlerbehandlung heraus first. Die größte Schwäche für Haskell in größeren Systemen ist derzeit die Fülle an Fehlerbehandlungsmethoden, einschließlich mieser Methoden wie "Vielleicht" (was falsch ist, weil Sie keine Informationen darüber zurückgeben können, was schief gelaufen ist) nur fehlende Werte bedeuten). Überlegen Sie sich zunächst, wie Sie vorgehen sollen, und richten Sie Adapter aus den verschiedenen Fehlerbehandlungsmechanismen ein, die Ihre Bibliotheken und anderer Code in Ihrem endgültigen Mechanismus verwenden. Dies wird Ihnen später eine Welt der Trauer ersparen.

Nachtrag (aus Kommentaren entnommen; danke an Lii & liminalisht ) -
Weitere Diskussion über verschiedene Möglichkeiten, ein großes Programm in Monaden in einem Stapel aufzuteilen:

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

118
user349653

Das Entwerfen großer Programme in Haskell unterscheidet sich nicht wesentlich von anderen Sprachen. Beim Programmieren im Großen und Ganzen geht es darum, Ihr Problem in überschaubare Teile zu unterteilen und diese zusammenzufügen. Die Implementierungssprache ist weniger wichtig.

Das heißt, in einem großen Design ist es schön, das Typensystem zu nutzen, um sicherzustellen, dass Sie Ihre Teile nur auf die richtige Weise zusammenfügen können. Dies kann neue Typen oder Phantomtypen umfassen, um Dinge, die den gleichen Typ zu haben scheinen, unterschiedlich zu machen.

Wenn es darum geht, den Code im Laufe der Zeit zu überarbeiten, ist Reinheit ein großer Segen. Versuchen Sie daher, den Code so weit wie möglich rein zu halten. Reiner Code lässt sich leicht umgestalten, da er keine versteckte Interaktion mit anderen Teilen Ihres Programms aufweist.

43
augustss

Mit dieses Buch habe ich das erste Mal strukturierte Funktionsprogrammierung gelernt. Es ist vielleicht nicht genau das, wonach Sie suchen, aber für Anfänger in der funktionalen Programmierung ist dies möglicherweise einer der besten ersten Schritte, um die Strukturierung funktionaler Programme zu erlernen - unabhängig von der Skala. Auf allen Abstraktionsebenen sollte das Design immer klar strukturiert sein.

Das Handwerk der funktionalen Programmierung

The Craft of Functional Programming

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

16
comonad

Ich schreibe gerade ein Buch mit dem Titel "Functional Design and Architecture". Es bietet Ihnen einen vollständigen Satz von Techniken zum Erstellen einer großen Anwendung unter Verwendung eines rein funktionalen Ansatzes. Es beschreibt viele Funktionsmuster und -ideen beim Erstellen einer SCADA-ähnlichen Anwendung 'Andromeda' zur Steuerung von Raumschiffen von Grund auf neu. Meine Hauptsprache ist Haskell. Das Buch umfasst:

  • Ansätze zur Architekturmodellierung anhand von Diagrammen;
  • Anforderungsanalyse;
  • Embedded DSL Domain Modellierung;
  • Externes DSL Design und Implementierung;
  • Monaden als Subsysteme mit Effekten;
  • Freie Monaden als funktionale Schnittstellen;
  • Pfeilförmige eDSLs;
  • Umkehrung der Kontrolle mit freien monadischen eDSLs;
  • Software-Transaktionsspeicher;
  • Linsen;
  • Zustand, Leser, Verfasser, RWS, ST-Monaden;
  • Unreiner Zustand: IORef, MVar, STM;
  • Multithreading und gleichzeitige Domänenmodellierung;
  • GUI;
  • Anwendbarkeit von Mainstream-Techniken und Ansätzen wie UML, SOLID, GRASP;
  • Interaktion mit unreinen Subsystemen.

Sie können sich mit dem Code für das Buch hier und dem 'Andromeda' Projektcode vertraut machen.

Ich rechne damit, dieses Buch Ende 2017 fertig zu stellen. Bis dahin können Sie meinen Artikel "Design und Architektur in der funktionalen Programmierung" (Rus) lesen hier .

[~ # ~] Update [~ # ~]

Ich habe mein Buch online geteilt (erste 5 Kapitel). Siehe Beitrag auf Reddit

11
graninas

Gabriels Blogpost Skalierbare Programmarchitekturen könnte eine Erwähnung wert sein.

Haskell-Designmuster unterscheiden sich in einer wichtigen Hinsicht von den gängigen Designmustern:

  • Konventionelle Architektur : Kombinieren Sie mehrere Komponenten des Typs A, um ein "Netzwerk" oder eine "Topologie" des Typs B zu generieren

  • Haskell-Architektur : Kombinieren Sie mehrere Komponenten des Typs A, um eine neue Komponente desselben Typs A zu erzeugen, deren Charakter sich nicht von ihren Substituenten unterscheidet

Es fällt mir oft auf, dass eine anscheinend elegante Architektur häufig aus Bibliotheken herausfällt, die dieses schöne Gefühl der Homogenität auf eine Art von unten nach oben aufweisen. In Haskell wird dies besonders deutlich: Muster, die traditionell als "Top-Down-Architektur" betrachtet werden, werden in Bibliotheken wie mvc , Netwire und Cloud Haskell) erfasst . Das heißt, ich hoffe, diese Antwort wird nicht als Versuch interpretiert, einen der anderen in diesem Thread zu ersetzen, sondern nur, dass strukturelle Entscheidungen idealerweise von Domain-Experten in Bibliotheken abstrahiert werden können und sollten. Die eigentliche Schwierigkeit beim Aufbau großer Systeme besteht meiner Meinung nach darin, diese Bibliotheken anhand ihrer architektonischen "Güte" im Vergleich zu all Ihren pragmatischen Bedenken zu bewerten.

Wie liminalisht in den Kommentaren erwähnt, The category design pattern ist ein weiterer Beitrag von Gabriel zum Thema, ähnlich.

7
Rehno Lindeque

Ich habe den Artikel " Teaching Software Architecture Using Haskell " (pdf) von Alejandro Serrano als nützlich für groß angelegte Überlegungen angesehen Struktur in Haskell.

5
haroldcarr

Vielleicht müssen Sie einen Schritt zurückgehen und überlegen, wie Sie die Beschreibung des Problems überhaupt in ein Design umsetzen können. Da Haskell so hoch ist, kann es die Beschreibung des Problems in Form von Datenstrukturen, die Aktionen als Prozeduren und die reine Transformation als Funktionen erfassen. Dann hast du ein Design. Die Entwicklung beginnt, wenn Sie diesen Code kompilieren und konkrete Fehler zu fehlenden Feldern, fehlenden Instanzen und fehlenden monadischen Transformern in Ihrem Code finden, da Sie beispielsweise einen Datenbankzugriff aus einer Bibliothek ausführen, die eine bestimmte Statusmonade innerhalb eines IO Prozedur. Und voila, da ist das Programm. Der Compiler füttert Ihre mentalen Skizzen und gibt Kohärenz für das Design und die Entwicklung.

Auf diese Weise profitieren Sie von Anfang an von der Hilfe von Haskell, und die Codierung ist natürlich. Ich würde mich nicht darum kümmern, etwas "Funktionales" oder "Reines" oder genug Allgemeines zu tun, wenn Sie ein konkretes gewöhnliches Problem im Sinn haben. Ich denke, Überentwicklung ist die gefährlichste Sache in der IT. Anders verhält es sich, wenn das Problem darin besteht, eine Bibliothek zu erstellen, die eine Reihe verwandter Probleme zusammenfasst.

3
agocorona