Was ist der Zweck von Pfeilen?

62

Ich lerne funktionales Programmieren mit Haskell und versuche, Konzepte zu erfassen, indem ich zuerst verstehe, warum ich sie benötige.

Ich möchte das Ziel von Pfeilen in funktionalen Programmiersprachen kennenlernen. Welches Problem lösen sie? Ich habe http://en.wikibooks.org/wiki/Haskell/Understanding_arrows und http://www.cse.chalmers.se/~rjmh/afp-arrows.pdf überprüft . Ich verstehe nur, dass sie verwendet werden, um Diagramme für Berechnungen zu beschreiben, und dass sie eine einfachere punktfreie Stilcodierung ermöglichen.

Der Artikel geht davon aus, dass point free style im Allgemeinen leichter zu verstehen und zu schreiben ist. Das scheint mir ziemlich subjektiv zu sein. In einem anderen Artikel ( http://en.wikibooks.org/wiki/Haskell/StephensArrowTutorial#Hangman:_Main_program ) ist ein Hangman-Spiel implementiert, aber ich kann nicht sehen, wie Pfeile diese Implementierung natürlich machen.

Ich konnte viele Papiere finden, die das Konzept beschreiben, aber nichts über die Motivation.

Was mir fehlt

Simon Bergot
quelle

Antworten:

42

Mir ist klar, dass ich zu spät zur Party komme, aber Sie hatten hier zwei theoretische Antworten und ich wollte eine praktische Alternative zum Kauen bieten. Ich komme als ein verwandter Haskell Noob, der dennoch kürzlich für ein Projekt, an dem ich gerade arbeite, zwangsmarschiert wurde.

Erstens können Sie die meisten Probleme in Haskell produktiv lösen, ohne nach Pfeilen zu greifen. Einige bemerkenswerte Haskeller mögen und benutzen sie wirklich nicht (mehr dazu hier , hier und hier ). Wenn du also zu dir selbst sagst "Hey, ich brauche diese nicht", dann verstehe, dass du vielleicht wirklich richtig bist.

Was ich an Arrows am meisten frustrierte, als ich sie zum ersten Mal lernte, war, wie die Tutorials zu diesem Thema unweigerlich zur Analogie der Schaltkreise führten. Wenn Sie sich den Arrow-Code ansehen - zumindest die gezuckerte Sorte -, ähnelt er weniger einer Hardware-Definitionssprache. Ihre Eingänge sind rechts ausgerichtet, Ihre Ausgänge links. Wenn Sie nicht alle richtig verkabeln, werden sie einfach nicht ausgelöst. Ich dachte mir: Wirklich? Sind wir hier gelandet? Haben wir eine Sprache geschaffen, die so hoch ist, dass sie wieder aus Kupferdrähten und Lötzinn besteht?

Die richtige Antwort auf diese Frage lautet, soweit ich feststellen konnte: Ja. Der derzeitige Killer-Anwendungsfall für Arrows ist FRP (denken Sie an Yampa, Spiele, Musik und reaktive Systeme im Allgemeinen). Das Problem, mit dem FRP konfrontiert ist, ist im Wesentlichen dasselbe wie bei allen anderen synchronen Nachrichtenübermittlungssystemen: Wie kann ein kontinuierlicher Strom von Eingaben in einen kontinuierlichen Strom von Ausgaben verdrahtet werden, ohne relevante Informationen zu verlieren oder Leckagen auszulösen? Sie können die Streams als Listen modellieren - mehrere aktuelle FRP-Systeme verwenden diesen Ansatz -, aber wenn Sie viele Eingaben haben, ist die Verwaltung von Listen fast unmöglich. Sie müssen sich vom Strom isolieren.

Was Pfeile in FRP-Systemen erlauben, ist die Zusammensetzung von Funktionen in einem Netzwerk, während gleichzeitig jeglicher Verweis auf die zugrunde liegenden Werte, die von diesen Funktionen übergeben werden, vollständig abstrahiert wird. Wenn Sie noch nicht mit FP vertraut sind, kann dies zunächst verwirrend und dann überwältigend sein, wenn Sie die Auswirkungen aufgegriffen haben. Sie haben erst vor kurzem die Idee aufgegriffen, dass Funktionen abstrahiert werden können und wie man eine Liste [(*), (+), (-)]als vom Typ versteht [(a -> a -> a)]. Mit Pfeilen können Sie die Abstraktion eine Ebene weiter verschieben.

Diese zusätzliche Fähigkeit zu abstrahieren birgt ihre eigenen Gefahren. Zum einen kann es GHC in Eckfälle drängen, in denen es nicht weiß, was es von Ihren Typannahmen halten soll. Sie müssen bereit sein, auf der Typebene zu denken - dies ist eine hervorragende Gelegenheit, sich mit Arten und RankNTypes und anderen derartigen Themen vertraut zu machen.

Es gibt auch eine Reihe von Beispielen für "Stupid Arrow Stunts", bei denen der Programmierer nach einem Pfeilkombinator greift, nur weil er oder sie mit Tupeln einen ordentlichen Trick vorführen möchte. (Hier ist mein trivialer Beitrag zum Wahnsinn .) Sie können dieses Hot-Dogging einfach ignorieren, wenn Sie es in freier Wildbahn sehen.

HINWEIS: Wie ich oben erwähnt habe, bin ich ein relativer Neuling. Wenn ich oben falsche Vorstellungen verkündet habe, können Sie mich gerne korrigieren.

rtperson
quelle
2
Ich bin froh, dass ich noch nichts akzeptiert habe. Vielen Dank für diese Antwort. Es ist mehr auf Benutzer ausgerichtet. Die Beispiele sind großartig. Die subjektiven Teile sind klar definiert und ausgewogen. Ich hoffe, dass Leute, die diese Frage aufgeworfen haben, wiederkommen und das sehen.
Simon Bergot
Zwar sind Pfeile definitiv das falsche Werkzeug für Ihre verknüpfte Lösung, aber ich glaube, ich muss erwähnen, dass removeAt' n = arr(\ xs -> (xs,xs)) >>> arr (take (n-1)) *** arr (drop n) >>> arr (uncurry (++)) >>> returnAdies präziser und klarer formuliert werden kann als removeAt' n = (arr (take $ n-1) &&& arr (drop n)) >>> (arr $ uncurry (++)).
Cemper93
29

Dies ist eine Art "weiche" Antwort, und ich bin mir nicht sicher, ob eine Referenz dies tatsächlich so aussagt, aber so habe ich mir Pfeile vorgestellt:

Ein Pfeiltyp A b cist im Grunde eine Funktion, die b -> cjedoch M astrukturierter ist als ein einfacher alter Wert a.

Wie diese zusätzliche Struktur aussieht, hängt von der jeweiligen Pfeilinstanz ab, über die Sie sprechen. Genau wie bei Monaden IO aund die Maybe ajeweils unterschiedliche zusätzliche Struktur haben.

Die Sache, die Sie mit Monaden bekommen, ist eine Unfähigkeit, von einem M azu einem zu gehen a. Nun mag dies wie eine Einschränkung erscheinen, aber es ist tatsächlich ein Merkmal: Das Typsystem schützt Sie davor, einen monadischen Wert in einen einfachen alten Wert umzuwandeln. Sie können den Wert nur nutzen, indem Sie an der Monade über >>=oder den primitiven Operationen der jeweiligen Monadeninstanz teilnehmen.

Ebenso ist die Sache, die Sie erhalten, A b ceine Unfähigkeit, eine neue b-verbrauchende c-produzierende "Funktion" zu konstruieren. Der Pfeil schützt Sie davor, die Ausnahme zu konsumieren bund zu erstellen, cindem Sie an den verschiedenen Pfeilkombinatoren teilnehmen oder die primitiven Operationen der jeweiligen Pfeilinstanz verwenden.

Zum Beispiel sind die Signalfunktionen in Yampa ungefähr (Time -> a) -> (Time -> b), aber zusätzlich müssen sie einer gewissen Kausalitätsbeschränkungt unterliegen : Die Ausgabe zum Zeitpunkt wird durch die vergangenen Werte des Eingangssignals bestimmt: Sie können nicht in die Zukunft schauen. Sie programmieren also nicht mit (Time -> a) -> (Time -> b), sondern mit SF a bund bauen Ihre Signalfunktionen aus Primitiven auf. Es kommt also vor, dass since SF a bsich stark wie eine Funktion verhält, sodass die gemeinsame Struktur ein sogenannter "Pfeil" ist.

Lambdageek
quelle
"Der Pfeil schützt Sie vor dem Konsumieren bund Erstellen einer cAusnahme, indem Sie an den verschiedenen Pfeilkombinatoren teilnehmen oder die primitiven Operationen der jeweiligen Pfeilinstanz verwenden." Mit Entschuldigung für die Antwort auf diese uralte Antwort: Dieser Satz ließ mich an lineare Typen denken, dh dass Ressourcen nicht geklont oder verschwunden werden können. Glaubst du, dass es einen Zusammenhang geben könnte?
Glaebhoerl
14

Ich denke an Pfeile wie Monaden und Funktoren, die es dem Programmierer ermöglichen, exotische Kompositionen von Funktionen zu erstellen.

Ohne Monaden oder Pfeile (und Funktoren) ist die Komposition von Funktionen in einer funktionalen Sprache darauf beschränkt, eine Funktion auf das Ergebnis einer anderen Funktion anzuwenden. Mit Monaden und Funktoren können Sie zwei Funktionen definieren und dann separaten wiederverwendbaren Code schreiben, der angibt, wie diese Funktionen im Kontext der jeweiligen Monade miteinander und mit den Daten, die an sie übergeben werden, interagieren. Dieser Code wird in den Bindungscode der Monade eingefügt. Eine Monade ist also nur eine Ansicht, nur ein Container für wiederverwendbaren Bindungscode. Funktionen setzen sich im Kontext einer Monade anders zusammen als eine andere Monade.

Ein einfaches Beispiel ist die Vielleicht-Monade, bei der die Bindefunktion Code enthält. Wenn also eine Funktion A mit einer Funktion B innerhalb einer Vielleicht-Monade zusammengesetzt ist und B ein Nichts erzeugt, stellt der Bindecode die Zusammensetzung der sicher Zwei Funktionen geben ein Nothing aus, ohne A auf den Nothing-Wert anzuwenden, der aus B kommt. Wenn es keine Monade gäbe, müsste der Programmierer Code in A schreiben, um auf eine Nothing-Eingabe zu testen.

Monaden bedeuten auch, dass der Programmierer die Parameter, die jede Funktion benötigt, nicht explizit in den Quellcode eingeben muss - die Bindefunktion übernimmt die Parameterübergabe. Bei der Verwendung von Monaden kann der Quellcode eher wie eine statische Kette von Funktionsnamen aussehen, als dass Funktion A die Funktion B mit den Parametern C und D "aufruft" - der Code fängt an, sich eher wie eine elektronische Schaltung als wie eine Bewegliche Maschine - mehr funktional als zwingend.

Pfeile verbinden Funktionen auch mit einer Bindefunktion, wodurch wiederverwendbare Funktionen bereitgestellt und Parameter ausgeblendet werden. Arrows können jedoch selbst miteinander verbunden und zusammengesetzt werden. Optional können Daten zur Laufzeit an andere Arrows weitergeleitet werden. Jetzt können Sie Daten auf zwei Pfeile anwenden, die die Daten "auf unterschiedliche Weise bearbeiten" und das Ergebnis wieder zusammensetzen. Sie können auch auswählen, an welchen Zweig von Pfeilen die Daten übergeben werden sollen, abhängig von einem bestimmten Wert in den Daten. Der resultierende Code ähnelt eher einer elektronischen Schaltung mit Schaltern, Verzögerungen, Integration usw. Das Programm sieht sehr statisch aus, und Sie sollten nicht in der Lage sein, die Manipulation von Daten zu beobachten. Es gibt immer weniger Parameter, über die nachgedacht werden muss, und es muss weniger darüber nachgedacht werden, welche Werte Parameter annehmen können oder nicht.

Das Schreiben eines Arrowized-Programms umfasst in der Regel die Auswahl von Standardpfeilen wie Splittern, Schaltern, Verzögerungen und Integratoren, das Heben von Funktionen in diese Pfeile und das Verbinden der Pfeile zu größeren Pfeilen. In Arrowized Functional Reactive Programming bilden die Pfeile eine Schleife, wobei Eingaben aus der Welt mit Ausgaben aus der letzten Iteration des Programms kombiniert werden, sodass die Ausgabe auf Eingaben aus der realen Welt reagiert.

Einer der wahren Werte ist die Zeit. In Yampa führt der Signalfunktionspfeil den Zeitparameter unsichtbar durch das Computerprogramm - Sie greifen nie auf den Zeitwert zu, aber wenn Sie einen Integratorpfeil in das Programm einbinden, werden über die Zeit integrierte Werte ausgegeben, die Sie dann weitergeben können andere Pfeile.

Robert Onslow
quelle
Dies klingt jedoch wie ein anwendungsbezogener Funktor (ein Wrapper um eine Funktion, der in einem bestimmten Kontext Hilfsfunktionen zur Wiederverwendung bereits vorhandener Funktionen für die Wrapped-Typen bereitstellt). Ich muss definitiv mehr lesen, um zu verstehen, aber vielleicht können Sie helfen, indem Sie darauf hinweisen, was ich vermisse
Belun
3

Nur eine Ergänzung zu den anderen Antworten: Persönlich hilft es mir sehr zu verstehen, was ein solches Konzept (mathematisch) ist und wie es sich auf andere Konzepte bezieht, die ich kenne.

Im Fall von Pfeilen fand ich das folgende Papier hilfreich - es vergleicht Monaden, anwendbare Funktoren (Redewendungen) und Pfeile: Redewendungen sind vergesslich, Pfeile sind akribisch, Monaden sind promiskuitiv von Sam Lindley, Philip Wadler und Jeremy Yallop.

Ich glaube auch, dass niemand diesen Link erwähnt hat, der Ihnen einige Ideen und Literatur zu diesem Thema liefern könnte.

Petr Pudlák
quelle