Ich habe ein Rätsel für dich,
Ich habe es geschafft, Code zu schreiben, der diese Dinge mit Rekursionsschemata machen würde, aber es ist unglaublich unordentlich und das bedeutet normalerweise, dass ich irgendwo eine nützliche Abstraktion verpasst habe.
Ich entwerfe ein Layout-System für meinen Texteditor
Rasa
; Es verwendet sehr ähnliche Splits
Art wie Vim. Ich beschloss, die Splits mit einem Baum zu beschreiben; du kannst dir vorstellen
es als binärer Baum von entweder vertikalen oder horizontalen Spaltungen mit 'Ansichten' an der
Blattknoten. Dies
Bild
könnte helfen.
Hier ist meine anfängliche Datenstruktur:
%Vor%Einige der benötigten Operationen sind:
split :: (View -> Tree View) -> Tree View -> Tree View
, die alle Ansichten, die mit dem Prädikat übereinstimmen, durch Entfernen schließt
sie vom Baum und ordnen benachbarte Ansichten richtig. Einige nette Features:
- close :: (View -> Bool) -> Tree View -> Tree View
, Setzt eine Ansicht so, dass sie nur dann aktiv ist, wenn die nächste horizontal verbunden ist
Ansicht nach links WAS aktiv
Ich suche nach einer Abstraktion oder einer Reihe von Abstraktionen, die dies ermöglichen würden Funktionalität auf eine saubere Art und Weise. Hier ist mein Denkprozess soweit:
Zuerst dachte ich, ich hätte ein Monoid. Die Identität war der leere Baum, und
fmap
würde einfach einen weiteren Zweig an den Baum anhängen, aber das funktioniert nicht
seit ich zwei Operationen habe: vertikal append und horizontal append und die Operationen
sind nicht assoziativ, wenn sie zusammengemischt sind.
Als nächstes dachte ich: "Einige meiner Operationen hängen von ihrem Kontext ab", also habe ich wahrscheinlich einen Comonad. Die Version des Baumes
Ich arbeite nicht als Co-Monade, weil ich keinen Wert für focusRight :: Tree View -> Tree View
in einem Zweig habe, also habe ich meinen Baum neu strukturiert
so:
aber das immer noch
Ich habe den Fall nicht behandelt, in dem ein Knoten basierend auf dem, was drin war, "aufgeteilt" wurde. Dies entsprach der Signatur mappend
, die mit extract
von Monad vereinheitlicht wurde, also hatte ich vielleicht eine Monade? Ich kann Monade für die implementieren
ursprüngliche Baumdefinition, kann es aber nicht für meine Comonad-Baumversion herausfinden.
Gibt es eine Möglichkeit, das Beste aus beiden Welten hier zu bekommen? Entgrabe ich mit Comonad / Monad den falschen Baum? Grundsätzlich suche ich nach einer eleganten Möglichkeit, diese Funktionen über meine Datenstruktur zu modellieren. Danke!
Wenn Sie den vollständigen Code sehen möchten, sind die Funktionen hier und der aktuelle Baum ist hier .
Ich habe es aufgegeben, das in einen Kommentar zu stopfen. Conor McBride hat eine ganze rede und mit Sam Lindley ein großer Teil eines Papiers , alles über die Verwendung von Monaden, um 2D-Raum zu schnitzen. Da Sie nach einer eleganten Lösung gefragt haben, bin ich gezwungen, Ihnen eine Zusammenfassung ihrer Arbeit zu geben, obwohl ich Ihnen nicht empfehlen würde, dies in Ihre Codebase zu integrieren - ich vermute, es ist wahrscheinlich einfacher, nur mit einer Bibliothek wie boxes
und die Ausschneide- und Größenänderungslogik manuell mit manueller Fehlerbehandlung ankurbeln.
Ihr erstes Tree
ist ein Schritt in die richtige Richtung. Wir können eine Monad
-Instanz schreiben, um Bäume miteinander zu verbinden:
Tree
s join
nimmt einen Baum mit Bäumen an seinen Blättern und lässt Sie den ganzen Weg laufen auf den Boden, ohne auf halbem Weg anhalten zu müssen. Es kann hilfreich sein, an Tree
als zu denken freie Monade , wie @danidiaz in eine Antwort gezeigt hat. Oder könnte Kmett sagen , dass Sie eine sehr einfache Syntax haben, die eine Terminusersetzung erlaubt, deren Var
Leaf
heißt.
Wie auch immer, der Punkt ist, dass Sie >>=
verwenden können, um Bäume zu züchten, indem Sie fortlaufend ihre Blätter zerhacken. Hier habe ich eine eindimensionale Benutzeroberfläche (vergessen wir% code% im Moment) mit einem einzigen Fenster, das ein Direction
enthält, und indem ich es wiederholt in zwei Hälften zerschneide, lande ich mit acht kleineren Fenstern.
(Im wirklichen Leben würden Sie wahrscheinlich nur ein Fenster nach dem anderen aufschneiden, indem Sie seine ID in Ihrer Bindungsfunktion überprüfen und unverändert zurückgeben, wenn es nicht die gesuchte ist.)
Das Problem ist, dass String
kein Verständnis dafür hat, dass der physische Raum eine begrenzte und wertvolle Ressource ist. Mit Tree
können Sie fmap
s durch a
s ersetzen, aber die resultierende Struktur passt nicht auf den Bildschirm, wenn b
s mehr Platz belegt als b
s!
Das wird in zwei Dimensionen ernster, weil Boxen sich gegenseitig herumdrücken und reißen können. Wenn das mittlere Fenster versehentlich verkleinert wird, bekomme ich entweder eine verformte Box oder ein Loch in der Mitte (abhängig vom Aufenthaltsort im Baum).
%Vor% Das Erweitern eines Fensters ist durchaus sinnvoll, aber in der realen Welt muss der Raum, in den es expandiert, irgendwo herkommen. Sie können kein Fenster vergrößern, ohne ein anderes zu verkleinern, und umgekehrt. Dies ist nicht die Art von Operation, die mit a
durchgeführt werden kann, die lokale Substitutionen an einzelnen Blattknoten ausführt; Sie müssen sich die Geschwister eines Fensters ansehen, um zu wissen, wer den angrenzenden Raum belegt.
Sie sollten also >>=
nicht verwenden dürfen, um den Inhalt so zu ändern. Lindley und McBride's Idee ist es, dem Typ-Checker beizubringen, wie man Boxen zusammenfügt. Verwenden von natürlichen Zahlen auf Typenebene und Hinzufügen,
Sie arbeiten mit Inhalten, die durch ihre Breite und Höhe indiziert sind. (In der Arbeit verwenden sie 2D-Matrizen, die als Vektoren von Vektoren dargestellt werden, aber aus Effizienzgründen möchten Sie vielleicht ein Array mit einem Phantom-Typ verwenden, der seine Größe misst.)
%Vor% Wenn Sie zwei Felder nebeneinander mit >>=
platzieren, müssen sie dieselbe Höhe haben. Wenn Sie sie mit Hor
übereinander platzieren, müssen sie dieselbe Breite haben.
Jetzt können wir eine Monade bauen, um diese Bäume zu verbinden. Die Semantik von Ver
hat sich nicht geändert - sie fügt ein 2D-Objekt in einem return
selbst hinzu.
Nun denken wir an Box
. Im Allgemeinen besteht eine Box aus einer Anzahl von Stücken unterschiedlicher Größe, die irgendwie zusammengesetzt sind, um eine größere Box zu erzeugen. Unten habe ich drei Teile mit den Größen 2x1, 2x2 und 1x3, die eine 3x3-Box ergeben. Diese Box sieht ungefähr wie >>=
aus.
Während Sie, der Aufrufer von Content
, die äußeren Maße Ihrer Box kennen, kennen Sie nicht die Dimensionen der einzelnen Teile des Inhalts, aus denen sie bestehen. Wie kann man erwarten, dass die Größe des Inhalts erhalten bleibt, wenn Sie ihn mit Hor (Ver (Content 2x1) (Content 2x2)) Content 1x3
schneiden? Sie müssen eine Funktion schreiben, die die Größe ohne a priori Wissen über die Größe behält.
Also >>=
nimmt ein >>=
einer bekannten Größe >>=
, zerlegt es, um den Inhalt zu finden, verarbeitet es mit einer Funktion, die die (unbekannte) Größe des Inhalts beibehält, den Sie ihm geben * und setzt es wieder zusammen, um eine neue Box mit der gleichen Größe Box
zu produzieren. Beachten Sie den Rang-2-Typ, der die Tatsache widerspiegelt, dass der Aufrufer von wh
keine Kontrolle über die Dimensionen des Inhalts hat, mit dem die Fortsetzung aufgerufen wird.
Wenn Sie ein Typ-Synonym wh
für indexerhaltende Funktionen verwenden und die Argumente umdrehen, erhalten Sie etwas, das für >>=
s nur wie ~>
aussieht, aber mit einer anderen Art von Pfeil. Kleisli Komposition kommt auch ziemlich hübsch aus.
Also das sind Monaden über indizierte Sets. (Mehr in Kleisli Arrows of Outrageous Fortune .) das Papier sie bauen ein Bündel mehr Infrastruktur, um das Zuschneiden und Neuanordnen von Boxen zu unterstützen wäre wahrscheinlich nützlich für den Aufbau einer Benutzeroberfläche. Aus Effizienzgründen können Sie das aktuell fokussierte Fenster auch mit einem Reißverschluss , was eine lustige Übung ist. Übrigens denke ich, dass Hasochismus eine großartige Einführung in die Phantasiearten im Allgemeinen ist, nicht nur als Lösung für dieses spezielle Problem.
* Angenommen, der =<<
Index ist in der Tat ein genaues Maß für seine physikalische Größe
Ich würde Ihren Typ als Monade darstellen und >>=
für split
verwenden.
Wie bei close
würde ich wahrscheinlich cata
oder para
von recursion-schemes , da close
scheinbar von unten nach oben funktioniert und höchstens Wissen über Knoteneltern und Geschwister benötigt. Sie können sich auch an Control.Lens.Plated
wenden .
Übrigens hat Free
bereits eine Recursive
Instanz. FreeF TreeF a
wäre die entsprechende Algebra. Aber du hast erwähnt, dass es nicht gut gelaufen ist.
Die direkte Arbeit mit den Bausteinen Free
und FreeT
könnte sich als umständlich erweisen. Vielleicht könnten ein paar Synonymen helfen.