Nehmen wir an, ich habe die folgende Logik. Wie schreibt man das in Functional Programming?
public int doSomeCalc(int[] array)
{
int answer = 0;
if(array!=null)
{
for(int e: array)
{
answer += e;
if(answer == 10) break;
if(answer == 150) answer += 100;
}
}
return answer;
}
Die Beispiele in den meisten Blogs, Artikeln ... Ich sehe nur den einfachen Fall einer einfachen mathematischen Funktion, die 'Summe' sagt. Aber ich habe eine ähnliche Logik wie oben in Java geschrieben und möchte diese in Clojure auf funktionalen Code migrieren. Wenn wir dies in FP nicht tun können, wird dies in den Werbeaktionen für FP nicht explizit angegeben.
Ich weiß, dass der obige Code absolut unerlässlich ist. Es wurde nicht mit dem Gedanken geschrieben, es in Zukunft auf FP zu migrieren.
break
undreturn answer
durch einreturn
innerhalb der Schleife ersetzt werden kann. In FP könnten Sie diese vorzeitige Rückkehr mithilfe von Fortsetzungen implementieren, siehe z. B. en.wikipedia.org/wiki/ContinuationtakeWhile
.Antworten:
Das nächste Äquivalent zum Durchlaufen eines Arrays in den meisten funktionalen Sprachen ist eine
fold
Funktion, dh eine Funktion, die eine benutzerdefinierte Funktion für jeden Wert des Arrays aufruft und einen akkumulierten Wert entlang der Kette weitergibt. In vielen funktionalen Sprachen wirdfold
es durch eine Reihe zusätzlicher Funktionen ergänzt, die zusätzliche Funktionen bieten, einschließlich der Option, bei bestimmten Bedingungen vorzeitig zu stoppen. In faulen Sprachen (z. B. Haskell) kann ein vorzeitiges Stoppen einfach dadurch erreicht werden, dass die Liste nicht weiter ausgewertet wird, wodurch niemals zusätzliche Werte generiert werden. Wenn ich Ihr Beispiel auf Haskell übersetze, würde ich es folgendermaßen schreiben:Wenn Sie die Syntax von Haskell nicht kennen, können Sie dies zeilenweise wie folgt aufteilen:
Definiert den Typ der Funktion, akzeptiert eine Liste von Ints und gibt einen einzelnen Int zurück.
Der Hauptteil der Funktion: Argument angegeben
values
, Rückgabefoldr1
mit Argumenten aufgerufencombine
(die wir unten definieren werden) undvalues
.foldr1
ist eine Variante des Fold-Primitivs, die mit dem auf den ersten Wert der Liste gesetzten Akkumulator beginnt (daher der1
im Funktionsnamen angegebene) und ihn dann mit der vom Benutzer angegebenen Funktion von links nach rechts kombiniert (was normalerweise als Rechtsfalte bezeichnet wird). daher dasr
im Funktionsnamen). Istfoldr1 f [1,2,3]
also gleichbedeutend mitf 1 (f 2 3)
(oderf(1,f(2,3))
in konventioneller C-ähnlicher Syntax).Definieren der
combine
lokalen Funktion: Sie erhält zwei Argumentev1
undv2
. Wennv1
10 ist, kehrt es gerade zurückv1
. In diesem Fall wird v2 nie ausgewertet , sodass die Schleife hier anhält.Wenn alternativ v1 den Wert 150 hat, werden weitere 100 hinzugefügt und v2 hinzugefügt.
Und wenn keine dieser Bedingungen zutrifft, addieren Sie einfach v1 zu v2.
Diese Lösung ist etwas spezifisch für Haskell, da die Tatsache, dass eine rechte Falte endet, wenn die Kombinationsfunktion ihr zweites Argument nicht auswertet, durch die verzögerte Auswertungsstrategie von Haskell verursacht wird. Ich kenne Clojure nicht, aber ich glaube, es wird streng ausgewertet, daher würde ich erwarten, dass es eine
fold
Funktion in seiner Standardbibliothek enthält, die eine spezielle Unterstützung für die vorzeitige Beendigung beinhaltet. Dies wird oft genanntfoldWhile
,foldUntil
oder ähnliches.Ein kurzer Blick auf die Dokumentation der Clojure-Bibliothek zeigt, dass sie sich in der Benennung von den meisten funktionalen Sprachen ein wenig unterscheidet und
fold
nicht das ist, wonach Sie suchen (es handelt sich um einen fortgeschritteneren Mechanismus zur Ermöglichung paralleler Berechnungen),reduce
der jedoch direkter ist Äquivalent. Eine vorzeitige Beendigung liegt vor, wenn diereduced
Funktion innerhalb Ihrer Kombinationsfunktion aufgerufen wird. Ich bin mir nicht 100% sicher, ob ich die Syntax verstehe, aber ich vermute, dass Sie nach so etwas suchen:NB: Beide Übersetzungen, Haskell und Clojure, sind für diesen speziellen Code nicht ganz richtig. Sie vermitteln jedoch den allgemeinen Kern der Sache - siehe Diskussion in den Kommentaren unten für spezifische Probleme mit diesen Beispielen.
quelle
v1 v2
sind verwirrend:v1
ist ein "Wert aus Array", aberv2
das akkumulierte Ergebnis. und Ihre Übersetzung ist fehlerhaft, ich glaube, die OP-Schleife wird beendet, wenn der akkumulierte (von links) Wert 10 erreicht, nicht irgendein Element im Array. Gleiche mit dem Erhöhen von 100. Wenn auf den Einsatz hier klappt, verwenden Sie die linke Falte mit der frühen Abfahrt, für Abwechslung auffoldlWhile
hier .(= v1 150)
Verwendung des vorherigen Wertsv2
(aka.e
) Wird zu diesem summiert.Breaking this down line by line in case you're not familiar with Haskell's syntax
-- Sie sind mein Held. Haskell ist mir ein Rätsel.Sie können es leicht in eine Rekursion umwandeln. Und es hat einen schönen schwanzoptimierten rekursiven Aufruf.
Pseudocode:
quelle
GOTO
. (Nicht ganz so schlimm, aber immer noch ziemlich umständlich.) Das Äquivalent zu einer Schleife ist, wie Jules sagt, eine geeignete Falte.GOTO
. Auf jeden Fall läuft es beim Kompilieren der Schwanzrekursion meist nur auf einewhile (true)
Schleife mit dem Funktionskörper hinaus, in der Early Return nur einebreak
Anweisung ist. Eine Falte ist, obwohl Sie Recht haben, eine Schleife, in Wirklichkeit eher eingeschränkt als ein allgemeines Schleifenkonstrukt. es ist eher wie eine for-each-SchleifeGOTO
ist, dass Sie fummelig nachvollziehen müssen, welche Argumente in welchem Zustand an den rekursiven Aufruf übergeben werden, um sicherzustellen, dass er sich tatsächlich wie beabsichtigt verhält. Dies ist weder in angemessenen imperativen Schleifen (in denen es ziemlich klar ist, was die Zustandsvariablen sind und wie sie sich bei jeder Iteration ändern) noch in naiven Rekursionen (in denen normalerweise nicht viel mit den Argumenten getan wird) und stattdessen in imperativen Schleifen erforderlich Das Ergebnis wird ganz intuitiv zusammengestellt. ...Die Antwort von Jules gefällt mir sehr gut , aber ich wollte zusätzlich auf etwas hinweisen, was die Leute an fauler funktionaler Programmierung oft vermissen, nämlich, dass nicht alles "in the loop" sein muss. Beispielsweise:
Sie können sehen, dass jeder Teil Ihrer Logik in einer separaten Funktion berechnet und dann zusammengesetzt werden kann. Dies ermöglicht kleinere Funktionen, die in der Regel leichter zu beheben sind. In Ihrem Spielzeugbeispiel erhöht dies möglicherweise die Komplexität, als sie entfernt, aber im Code der realen Welt sind die Funktionen zum Aufteilen oft viel einfacher als das Ganze.
quelle
stopAt10
ist kein guter Verbraucher. Ihre Antwort ist besser als die, die Sie zitieren, da sie den grundlegenden Produzentenscanl (+) 0
von Werten korrekt isoliert . Ihr Verbrauch sollte die Steuerlogik jedoch direkt einbeziehen, ist besser mit nur zweispan
s und alast
explizit zu implementieren . das würde auch der ursprünglichen Codestruktur und -logik genau folgen und wäre leicht zu pflegen.Die meisten Listenverarbeitung Beispiele finden Sie Funktionen verwenden , wie sehen
map
,filter
,sum
etc. , die auf der Liste arbeiten als Ganzes. In Ihrem Fall haben Sie jedoch eine bedingte vorzeitige Beendigung - ein eher ungewöhnliches Muster, das von den üblichen Listenoperationen nicht unterstützt wird. Sie müssen also eine Abstraktionsebene ausklappen und eine Rekursion verwenden - was auch dem imperativen Beispiel näher kommt.Dies ist eine ziemlich direkte (wahrscheinlich nicht idiomatische) Übersetzung in Clojure:
Edit: Jules weisen darauf hin , dass
reduce
in Clojure tun vorzeitiges Ausscheiden unterstützen. Dies zu benutzen ist eleganter:Auf jeden Fall können Sie in funktionalen Sprachen alles tun, wie Sie es in imperativen Sprachen können. Oft müssen Sie jedoch Ihre Denkweise etwas ändern, um eine elegante Lösung zu finden. Bei der imperativen Codierung denken Sie daran, eine Liste schrittweise zu verarbeiten, während Sie bei funktionalen Sprachen nach einer Operation suchen, die auf alle Elemente in der Liste angewendet werden kann.
quelle
reduce
Operation von Clojure unterstützt das vorzeitige Beenden.takeWhile
nicht um eine "gemeinsame Operation"?takeWhile
ist eine häufige Operation, in diesem Fall jedoch nicht besonders nützlich, da Sie die Ergebnisse Ihrer Transformation benötigen, bevor Sie entscheiden können, ob Sie anhalten möchten . In einer faulen Sprache spielt das keine Rolle: Sie könnenscan
undtakeWhile
für die Ergebnisse des Scans verwenden (siehe Karl Bielefeldts Antwort, die, obwohl sie nicht verwendet wird,takeWhile
leicht umgeschrieben werden kann), aber für eine strenge Sprache wie clojure würde dies der Fall sein bedeutet, die gesamte Liste zu verarbeiten und anschließend die Ergebnisse zu verwerfen. Generatorfunktionen könnten dies jedoch lösen, und ich glaube, Clojure unterstützt sie.take-while
in Clojure erzeugt eine faule Sequenz (gemäß den Dokumenten). Eine andere Möglichkeit, dies zu lösen, wären Schallköpfe (vielleicht die besten).Wie andere Antworten zeigen, hat Clojure
reduced
dafür gesorgt, dass Reduzierungen vorzeitig eingestellt werden:Dies ist die beste Lösung für Ihre spezielle Situation. Sie können auch eine Menge Kilometer aus der Kombination bekommen
reduced
mittransduce
, was Sie Wandler verwenden kann , um ausmap
,filter
etc. Aber es ist weit von einer vollständigen Antwort auf Ihre allgemeine Frage ist.Escape-Fortsetzungen sind eine verallgemeinerte Version von break- und return-Anweisungen. Sie sind in einigen Schemes (
call-with-escape-continuation
), Common Lisp (block
+return
,catch
+throw
) und sogar C (setjmp
+longjmp
) direkt implementiert . Allgemeinere begrenzte oder nicht begrenzte Fortsetzungen wie im Standardschema oder als Fortsetzungsmonaden in Haskell und Scala können auch als Escape-Fortsetzungen verwendet werden.In Racket könnten Sie beispielsweise Folgendes verwenden
let/ec
:Viele andere Sprachen haben auch Escape Continuation-ähnliche Konstrukte in Form von Ausnahmebehandlung. In Haskell könnte man auch eine der verschiedenen Fehlermonaden mit verwenden
foldM
. Da es sich in erster Linie um Fehlerbehandlungskonstrukte handelt, die Ausnahmen oder Fehlermonaden für frühe Rückgaben verwenden, ist dies in der Regel kulturell inakzeptabel und möglicherweise recht langsam.Sie können auch von Funktionen höherer Ordnung zu Schlussanrufen wechseln.
Wenn Sie Schleifen verwenden, geben Sie die nächste Iteration automatisch ein, wenn Sie das Ende des Schleifenkörpers erreichen. Sie können die nächste Iteration frühzeitig mit eingeben
continue
oder die Schleife mitbreak
(oderreturn
) verlassen. Wenn Sie Tail-Aufrufe (oder dasloop
Konstrukt von Clojure, das die Tail-Rekursion nachahmt) verwenden, müssen Sie immer einen expliziten Aufruf ausführen, um die nächste Iteration einzugeben. Um die Schleife zu beenden, rufen Sie einfach nicht rekursiv auf, sondern geben den Wert direkt ein:quelle
MonadError
, ist das grundlegende ÄquivalentEither
nicht nur auf die Fehlerbehandlung ausgerichtet und kann daher leicht als Ersatz verwendet werden.Der komplizierte Teil ist die Schleife. Beginnen wir damit. Eine Schleife wird normalerweise in einen funktionalen Stil umgewandelt, indem die Iteration mit einer einzelnen Funktion ausgedrückt wird. Eine Iteration ist eine Transformation der Schleifenvariablen.
Hier ist eine funktionale Implementierung einer allgemeinen Schleife:
Es dauert (ein Anfangswert der Schleifenvariablen, die Funktion, die eine einzelne Iteration [in der Schleifenvariablen] ausdrückt) (eine Bedingung, um die Schleife fortzusetzen).
In Ihrem Beispiel wird eine Schleife in einem Array verwendet, die ebenfalls unterbrochen wird. Diese Fähigkeit in Ihrer imperativen Sprache ist in die Sprache selbst eingebettet. In der funktionalen Programmierung wird eine solche Fähigkeit normalerweise auf Bibliotheksebene implementiert. Hier ist eine mögliche Implementierung
Drin :
Ich benutze ein ((val, next_pos)) Paar, das die außerhalb sichtbare Schleifenvariable und die Position im Array enthält, die diese Funktion verbirgt.
Die Iterationsfunktion ist etwas komplexer als in der allgemeinen Schleife. Mit dieser Version kann das aktuelle Element des Arrays verwendet werden. [Es ist in Curryform .]
Solche Funktionen werden üblicherweise "fold" genannt.
Ich setze ein "l" in den Namen, um anzuzeigen, dass die Akkumulation der Elemente des Arrays linksassoziativ erfolgt. die Gewohnheit imperativer Programmiersprachen nachzuahmen, ein Array vom niedrigen zum hohen Index zu durchlaufen.
Ich habe ein "c" in den Namen eingefügt, um anzuzeigen, dass diese Fold-Version eine Bedingung annimmt, die steuert, ob und wann die Schleife vorzeitig gestoppt werden soll.
Natürlich sind solche Hilfsprogrammfunktionen wahrscheinlich in der Basisbibliothek verfügbar, die mit der verwendeten funktionalen Programmiersprache geliefert wird. Ich habe sie hier zur Demonstration geschrieben.
Nachdem wir alle Tools haben, die im Imperativfall in der Sprache verfügbar sind, können wir uns an die Implementierung der spezifischen Funktionalität Ihres Beispiels wenden.
Die Variable in Ihrer Schleife ist ein Paar ('answer', ein Boolescher Wert, der codiert, ob fortgefahren werden soll).
Beachten Sie, dass ich eine neue "Variable" "new_answer" verwendet habe. Dies liegt daran, dass ich in der funktionalen Programmierung den Wert einer bereits initialisierten "Variablen" nicht ändern kann. Ich mache mir keine Sorgen um die Leistung, der Compiler kann den Speicher von 'answer' für 'new_answer' über die Lebenszeitanalyse wiederverwenden, wenn er dies für effizienter hält.
Dies in unsere zuvor entwickelte Schleifenfunktion zu integrieren:
"Array" ist hier der Name des Moduls, dessen Exportfunktion foldlc lautet.
"Faust", "Sekunde" stehen für Funktionen, die die erste, zweite Komponente ihres Paarparameters zurückgeben
In diesem Fall erhöht der "punktfreie" Stil die Lesbarkeit der Implementierung von doSomeCalc:
(>>>) ist Funktionszusammensetzung:
(>>>) : (a -> b) -> (b -> c) -> (a -> c)
Es ist dasselbe wie oben, nur der Parameter "arr" wird auf beiden Seiten der Definitionsgleichung weggelassen.
Eine letzte Sache: Prüfung auf Groß- und Kleinschreibung (Array == Null). In besser gestalteten Programmiersprachen, aber auch in schlecht gestalteten Sprachen mit einigen grundlegenden Disziplinen verwendet man eher einen optionalen Typ , um Nichtexistenz auszudrücken. Das hat nicht viel mit funktionaler Programmierung zu tun, worum es letztendlich geht, also beschäftige ich mich nicht damit.
quelle
Schreiben Sie die Schleife zunächst leicht um, sodass jede Iteration der Schleife entweder vorzeitig beendet wird oder
answer
genau einmal mutiert :Es sollte klar sein, dass das Verhalten dieser Version genau dasselbe ist wie zuvor, aber jetzt ist es viel einfacher, sie in einen rekursiven Stil umzuwandeln. Hier ist eine direkte Übersetzung von Haskell:
Jetzt ist es rein funktional, aber wir können es sowohl vom Standpunkt der Effizienz als auch der Lesbarkeit aus verbessern, indem wir eine Falte anstelle einer expliziten Rekursion verwenden:
In diesem Zusammenhang wird
Left
frühzeitig mit seinem Wert beendet undRight
die Rekursion mit seinem Wert fortgesetzt.Dies könnte jetzt ein bisschen mehr vereinfacht werden:
Dies ist besser als der endgültige Haskell-Code, aber es ist jetzt etwas weniger klar, wie er auf das ursprüngliche Java zurückgeht.
quelle