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? :)
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
Der Profiler
Reinheit
Testen
Monaden zur Strukturierung
Typklassen und existenzielle Typen
Parallelität und Parallelität
par
in Ihr Programm ein, um die Konkurrenz mit einfacher, komponierbarer Parallelität zu schlagen.Refactor
Verwenden Sie den FFI mit Bedacht
Meta-Programmierung
Verpackung und Vertrieb
Warnungen
-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.
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.
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.
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.
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.
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
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:
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
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.
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.
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.