Was ist ein guter Weg, um 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 für einen bestimmten Zweck bestimmt. 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 Arten verarbeiten, in Benutzeroberflächen anzeigen, beibehalten, über Netzwerke kommunizieren usw. Wie könnte Eine beste Struktur, um solchen Code lesbar, wartbar und an sich ändernde Anforderungen anpassbar zu machen?
Es gibt eine ziemlich große Literatur, die sich mit diesen Fragen für große objektorientierte imperative Programme befasst. Ideen wie MVC, Entwurfsmuster usw. sind anständige Vorschriften zur Verwirklichung umfassender Ziele wie der Trennung von Bedenken und der 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 exotischer Kontrollstrukturen, die in der funktionalen Programmierung verfügbar sind (Monaden, Pfeile, Applikative usw.), für diesen Zweck am besten eingesetzt? Welche Best Practices können Sie empfehlen?
Vielen Dank!
EDIT (dies ist eine Fortsetzung 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 reinen 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 dem anderer Daten ab. Auf einige der Schritte sollten Nebenwirkungen wie GUI-Updates oder Datenbankabfragen folgen.
Was ist der richtige Weg, um die Daten und die Parsing-Schritte 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 könnte eine Monade verwenden, um zu verfolgen, was bisher verarbeitet wurde, und jeden Verarbeitungsschritt vom Monadenstatus alles bekommen zu lassen, was er als nächstes benötigt. Oder man könnte weitgehend separate Programme schreiben und Nachrichten senden (diese Option gefällt mir nicht besonders).
Die Folien, die er verlinkt hat, haben eine Aufzählungszeichen: "Redewendungen für die Zuordnung von Design zu Typen / Funktionen / Klassen / Monaden". Was sind die Redewendungen? :) :)
Antworten:
Ich spreche ein wenig darüber in Engineering Large Projects in Haskell und im Design und der Implementierung von XMonad. Beim Engineering im Großen und Ganzen geht es um das Management von Komplexität. Die primären Codestrukturierungsmechanismen in Haskell zur Verwaltung der Komplexität sind:
Das Typsystem
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
Sie diese Option , um Ihren Code geruchsfrei zu halten. Sie können sich auch Agda, Isabelle oder Catch ansehen, um mehr Sicherheit zu erhalten. Informationen zur fusselartigen Überprüfung finden Sie in der großen Anleitung , die 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 pflegen ist, da er kompositorisch ist. Das ist nicht immer möglich, aber es lohnt sich, darauf zu zielen.
Im Allgemeinen: Zerlegen Sie die logischen Einheiten Ihres Systems in die kleinstmöglichen referenziell transparenten Komponenten und implementieren Sie sie dann in Modulen. 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.
quelle
Don hat Ihnen die meisten der oben genannten Details gegeben, aber hier sind meine zwei Cent, wenn ich wirklich knifflige Stateful-Programme wie System-Daemons in Haskell mache.
Am Ende leben Sie in einem Monadentransformator-Stack. Unten ist IO. Darüber hinaus ordnet jedes Hauptmodul (im abstrakten Sinne, nicht im Sinne eines Moduls in einer Datei) seinen erforderlichen Zustand einer Ebene in diesem Stapel zu. 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 ihre Verbindung immer ohne Funktionen von anderen erhalten Module müssen sich ihrer Existenz bewusst sein. Möglicherweise haben Sie eine Schicht mit Ihrer Datenbankverbindung, eine andere mit Ihrer Konfiguration, eine dritte mit Ihren verschiedenen Semaphoren und MVARs für die Auflösung von Parallelität und Synchronisation, eine andere mit Ihren Protokolldateien usw.
Herauszufinden , Ihre Fehlerbehandlung zuerst . Die derzeit größte Schwäche von Haskell in größeren Systemen ist die Fülle von Fehlerbehandlungsmethoden, einschließlich mieser Methoden wie "Vielleicht" (was falsch ist, weil Sie keine Informationen darüber zurückgeben können, was schief gelaufen ist. Verwenden Sie immer "Entweder" statt "Vielleicht", es sei denn, Sie tun es wirklich nur fehlende Werte bedeuten). Finden Sie heraus, wie Sie es zuerst tun werden, und richten Sie Adapter aus den verschiedenen Fehlerbehandlungsmechanismen ein, die Ihre Bibliotheken und anderer Code in Ihrem endgültigen verwenden. Dies wird Ihnen später eine Welt der Trauer ersparen.
Nachtrag (aus Kommentaren extrahiert; danke an Lii & liminalisht ) -
mehr Diskussion über verschiedene Möglichkeiten, ein großes Programm in Monaden in einem Stapel aufzuteilen:
Ben Kolera gibt eine großartige praktische Einführung in dieses Thema, und Brian Hurt diskutiert Lösungen für das Problem,
lift
monadische Aktionen in Ihre benutzerdefinierte Monade zu integrieren. George Wilson zeigt, wie Siemtl
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.quelle
lift
monadische Aktionen in Ihre benutzerdefinierte Monade zu integrieren. George Wilson zeigt, wie Siemtl
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 Programmen. Beim Programmieren im großen Stil geht es darum, Ihr Problem in überschaubare Teile zu zerlegen und diese zusammenzufügen. Die Implementierungssprache ist weniger wichtig.
Trotzdem ist es in einem großen Design schön, das Typensystem zu nutzen, um sicherzustellen, dass Sie Ihre Teile nur auf die richtige Weise zusammenfügen können. Dies kann Newtype- oder Phantomtypen beinhalten, damit Dinge, die den gleichen Typ zu haben scheinen, unterschiedlich sind.
Wenn es darum geht, den Code im Laufe der Zeit umzugestalten, ist Reinheit ein großer Segen. Versuchen Sie also, so viel Code wie möglich rein zu halten. Reiner Code lässt sich leicht umgestalten, da er keine versteckte Interaktion mit anderen Teilen Ihres Programms aufweist.
quelle
Mit diesem Buch habe ich zum ersten Mal strukturierte funktionale Programmierung 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 zu lernen, funktionale Programme zu strukturieren - unabhängig von der Skala. Auf allen Abstraktionsebenen sollte das Design immer klar angeordnete Strukturen haben.
Das Handwerk der funktionalen Programmierung
http://www.cs.kent.ac.uk/people/staff/sjt/craft2e/
quelle
where beginner=do write $ tutorials `about` Monads
)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 funktionale Muster und Ideen beim Aufbau einer SCADA-ähnlichen Anwendung 'Andromeda' zur Steuerung von Raumschiffen von Grund auf neu. Meine Hauptsprache ist Haskell. Das Buch behandelt:
Sie können mit dem Code für das Buch vertraut hier , und der ‚Andromeda‘ Projektcode.
Ich erwarte , dass dieses Buch am Ende 2017. beenden Bis das geschieht, können Sie meinen Artikel „Design and Architecture in Functional Programming“ (Rus) lesen Sie hier .
AKTUALISIEREN
Ich habe mein Buch online geteilt (erste 5 Kapitel). Siehe Beitrag auf Reddit
quelle
Gabriels Blog-Beitrag Skalierbare Programmarchitekturen könnten eine Erwähnung wert sein.
Es fällt mir oft auf, dass eine scheinbar elegante Architektur oft aus Bibliotheken herausfällt, die dieses schöne Gefühl der Homogenität von unten nach oben aufweisen. In Haskell ist dies besonders deutlich - Muster, die traditionell als "Top-Down-Architektur" bezeichnet werden, werden in der Regel 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 von Domain-Experten in Bibliotheken idealerweise abstrahiert werden können und sollten. Die eigentliche Schwierigkeit beim Aufbau großer Systeme besteht meiner Meinung nach darin, diese Bibliotheken hinsichtlich ihrer architektonischen "Güte" im Vergleich zu all Ihren pragmatischen Anliegen zu bewerten.
Wie liminalisht in den Kommentaren erwähnt, In der Kategorie Design - Muster ist ein weiterer Beitrag von Gabriel zum Thema, in ähnlicher Weise.
quelle
Ich fand das Papier " Teaching Software Architecture Using Haskell " (pdf) von Alejandro Serrano nützlich, um über großräumige Strukturen in Haskell nachzudenken.
quelle
Vielleicht müssen Sie einen Schritt zurücktreten und überlegen, wie Sie die Beschreibung des Problems überhaupt in ein Design übersetzen können. Da Haskell auf so hohem Niveau ist, kann es die Beschreibung des Problems in Form von Datenstrukturen, die Aktionen als Prozeduren und die reine Transformation als Funktionen erfassen. Dann haben Sie ein Design. Die Entwicklung beginnt, wenn Sie diesen Code kompilieren und konkrete Fehler in Bezug auf fehlende Felder, fehlende Instanzen und fehlende monadische Transformatoren in Ihrem Code finden, da Sie beispielsweise einen Datenbankzugriff von einer Bibliothek aus durchführen, die eine bestimmte Statusmonade innerhalb einer E / A-Prozedur benötigt. Und voila, da ist das Programm. Der Compiler füttert Ihre mentalen Skizzen und gibt dem Design und der Entwicklung Kohärenz.
Auf diese Weise profitieren Sie von Anfang an von der Hilfe von Haskell, und die Codierung ist natürlich. Ich würde nicht gerne etwas "Funktionales" oder "Reines" oder genug Allgemeines tun, wenn das, was Sie im Sinn haben, ein konkretes gewöhnliches Problem ist. Ich denke, dass Over-Engineering das Gefährlichste in der IT ist. Anders sieht es aus, wenn das Problem darin besteht, eine Bibliothek zu erstellen, die eine Reihe verwandter Probleme abstrahiert.
quelle