In einem früheren Beitrag zum Thema Curry haben wir gesehen, wie Funktionen mit mehreren Parametern mit einem Parameter in kleinere Funktionen aufgeteilt werden. Dies ist eine mathematisch korrekte Lösung, aber es gibt noch andere Gründe dafür - sie führt auch zu einer sehr leistungsfähigen Technik, die als partielle Anwendung von Funktionen bezeichnet wird . Dieser Stil wird in der funktionalen Programmierung sehr häufig verwendet, und es ist sehr wichtig, ihn zu verstehen.

Teilweise Nutzung von Funktionen
Die Idee einer Teilanwendung ist, dass wir, wenn wir die ersten N Parameter der Funktion festlegen, eine neue Funktion mit den verbleibenden Parametern erhalten. Aus der Diskussion über das Currying konnte man ersehen, wie eine teilweise Anwendung auf natürliche Weise erfolgt.
Einige einfache Beispiele zur Veranschaulichung:
// "" + 42 let add42 = (+) 42 // add42 1 add42 3 // // [1;2;3] |> List.map add42 // "" let twoIsLessThan = (<) 2 // twoIsLessThan 1 twoIsLessThan 3 // twoIsLessThan [1;2;3] |> List.filter twoIsLessThan // "" printfn let printer = printfn "printing param=%i" // printer [1;2;3] |> List.iter printer
In jedem Fall erstellen wir eine teilweise angewendete Funktion, die in verschiedenen Situationen wiederverwendet werden kann.
Und natürlich macht es eine teilweise Anwendung ebenso einfach, Funktionsparameter festzulegen. Hier einige Beispiele:
// List.map let add1 = (+) 1 let add1ToEach = List.map add1 // "add1" List.map // add1ToEach [1;2;3;4] // List.filter let filterEvens = List.filter (fun i -> i%2 = 0) // // filterEvens [1;2;3;4]
Das folgende, komplexere Beispiel zeigt, wie mit demselben Ansatz transparent „eingebettetes“ Verhalten erstellt werden kann.
- Wir erstellen eine Funktion, die zwei Zahlen addiert, aber zusätzlich eine Protokollierungsfunktion benötigt, die diese Zahlen und das Ergebnis protokolliert.
- Die Protokollierungsfunktion akzeptiert zwei Parameter: (Zeichenfolge) "Name" und (generisch) "Wert", daher hat sie die Signaturzeichenfolge
string->'a->unit
. - Anschließend erstellen wir verschiedene Implementierungen der Protokollierungsfunktion, z. B. einen Konsolenlogger oder einen Popup-basierten Logger.
- Und schließlich wenden wir die Hauptfunktion teilweise an, um eine neue Funktion mit einem geschlossenen Logger zu erstellen.
// - let adderWithPluggableLogger logger xy = logger "x" x logger "y" y let result = x + y logger "x+y" result result // - let consoleLogger argName argValue = printfn "%s=%A" argName argValue // let addWithConsoleLogger = adderWithPluggableLogger consoleLogger addWithConsoleLogger 1 2 addWithConsoleLogger 42 99 // - let popupLogger argName argValue = let message = sprintf "%s=%A" argName argValue System.Windows.Forms.MessageBox.Show( text=message,caption="Logger") |> ignore // - let addWithPopupLogger = adderWithPluggableLogger popupLogger addWithPopupLogger 1 2 addWithPopupLogger 42 99
Diese Closed-Logger-Funktionen können wie alle anderen Funktionen verwendet werden. Beispielsweise können wir eine Teilanwendung zum Hinzufügen von 42 erstellen und diese dann wie bei der einfachen Funktion add42
die add42
.
// 42 let add42WithConsoleLogger = addWithConsoleLogger 42 [1;2;3] |> List.map add42WithConsoleLogger [1;2;3] |> List.map add42 //
Teilweise angewendete Funktionen sind ein sehr nützliches Werkzeug. Wir können flexible (wenn auch komplexe) Bibliotheksfunktionen erstellen, und es ist einfach, sie standardmäßig wiederverwendbar zu machen, damit die Komplexität vor dem Clientcode verborgen bleibt.
Teilfunktionsdesign
Offensichtlich kann die Reihenfolge der Parameter die Bequemlichkeit der teilweisen Verwendung ernsthaft beeinträchtigen. Beispielsweise haben die meisten Funktionen in List
wie List.map
und List.filter
eine ähnliche Form, nämlich:
List-function [function parameter(s)] [list]
Die Liste ist immer der letzte Parameter. Einige Beispiele in voller Form:
List.map (fun i -> i+1) [0;1;2;3] List.filter (fun i -> i>1) [0;1;2;3] List.sortBy (fun i -> -i ) [0;1;2;3]
Die gleichen Beispiele mit Teilanwendung:
let eachAdd1 = List.map (fun i -> i+1) eachAdd1 [0;1;2;3] let excludeOneOrLess = List.filter (fun i -> i>1) excludeOneOrLess [0;1;2;3] let sortDesc = List.sortBy (fun i -> -i) sortDesc [0;1;2;3]
Wenn Bibliotheksfunktionen mit einer anderen Reihenfolge von Argumenten implementiert würden, wäre eine teilweise Anwendung viel weniger bequem.
Wenn Sie Ihre Funktion mit vielen Parametern schreiben, können Sie über deren beste Reihenfolge nachdenken. Wie bei allen Designproblemen gibt es keine „richtige“ Antwort, aber es gibt mehrere allgemein akzeptierte Empfehlungen.
- Beginnen Sie mit Optionen, die wahrscheinlich statisch sind.
- Seien Sie der Letzte, der Datenstrukturen oder Sammlungen (oder andere sich ändernde Parameter) festlegt.
- Für ein besseres Verständnis von Operationen wie Subtraktion ist es ratsam, die erwartete Reihenfolge einzuhalten
Der erste Tipp ist einfach. Parameter, die wahrscheinlich durch Teilanwendung "behoben" werden, sollten an erster Stelle stehen, wie in den Beispielen mit dem obigen Logger.
Das Befolgen der zweiten Spitze erleichtert die Verwendung des Pipelining-Operators und der Zusammensetzung. Wir haben dies bereits viele Male in den Beispielen mit Funktionen über Listen gesehen.
// let result = [1..10] |> List.map (fun i -> i+1) |> List.filter (fun i -> i>5)
In ähnlicher Weise können teilweise angewendete Funktionen über Listen leicht der Komposition ausgesetzt werden, weil Listenparameter können weggelassen werden:
let compositeOp = List.map (fun i -> i+1) >> List.filter (fun i -> i>5) let result = compositeOp [1..10]
Teilweise BCL-Funktionsumhüllung
Die BCL-Funktionen (.NET Base Class Library) von .NET sind über F # leicht zugänglich, sie sind jedoch ohne Abhängigkeit von funktionalen Sprachen wie F # konzipiert. Beispielsweise erfordern die meisten Funktionen zu Beginn einen Datenparameter, während in F # der Datenparameter im Allgemeinen der letzte sein sollte.
Sie können jedoch leicht Wrapper schreiben, um diese Funktionen idiomatischer zu gestalten. Im folgenden Beispiel werden .NET-Zeichenfolgenfunktionen so umgeschrieben, dass die Zielzeichenfolge zuletzt und nicht zuerst verwendet wird:
// .NET let replace oldStr newStr (s:string) = s.Replace(oldValue=oldStr, newValue=newStr) let startsWith lookFor (s:string) = s.StartsWith(lookFor)
Nachdem die Zeichenfolge zum letzten Parameter geworden ist, können Sie diese Funktionen wie gewohnt in Pipelines verwenden:
let result = "hello" |> replace "h" "j" |> startsWith "j" ["the"; "quick"; "brown"; "fox"] |> List.filter (startsWith "f")
oder in der Zusammensetzung von Funktionen:
let compositeOp = replace "h" "j" >> startsWith "j" let result = compositeOp "hello"
Den Förderer bedienen
Nachdem Sie eine Teilanwendung im Geschäft gesehen haben, können Sie verstehen, wie Pipeline-Funktionen funktionieren.
Die Pipelining-Funktion ist wie folgt definiert:
let (|>) xf = fx
Alles, was sie tut, ist ein Argument vor die Funktion zu stellen, nicht danach.
let doSomething xyz = x+y+z doSomething 1 2 3 //
In dem Fall, in dem die Funktion f
mehrere Parameter hat und der letzte Parameter der Funktion f
als Eingabewert x
Pipeline fungiert. Tatsächlich wurde die übertragene Funktion f
bereits teilweise angewendet und erwartet nur einen Parameter - den Eingabewert für das Pipelining (te x
).
Hier ist ein ähnliches Beispiel, das für die teilweise Verwendung umgeschrieben wurde.
let doSomething xy = let intermediateFn z = x+y+z intermediateFn // intermediateFn let doSomethingPartial = doSomething 1 2 doSomethingPartial 3 // 3 |> doSomethingPartial // ,
Wie Sie gesehen haben, ist der Pipeline-Operator in F # äußerst verbreitet und wird immer dann verwendet, wenn Sie den natürlichen Datenfluss beibehalten möchten. Einige weitere Beispiele, auf die Sie möglicherweise gestoßen sind:
"12" |> int // "12" int 1 |> (+) 2 |> (*) 3 //
Reverse Conveyor Operator
Von Zeit zu Zeit kann der inverse Pipeline-Operator "<|" auftreten.
let (<|) fx = fx
Diese Funktion scheint nichts zu bewirken. Warum existiert sie also?
Der Grund dafür ist, dass bei Verwendung des inversen Pipeline-Operators als binärer Operator im Infix-Stil weniger Klammern erforderlich sind, wodurch der Code sauberer wird.
printf "%i" 1+2 // printf "%i" (1+2) // printf "%i" <| 1+2 //
Sie können Pipelines in zwei Richtungen gleichzeitig verwenden, um die Pseudo-Infix-Notation zu erhalten.
let add xy = x + y (1+2) add (3+4) // 1+2 |> add <| 3+4 //
Zusätzliche Ressourcen
Es gibt viele Tutorials für F #, einschließlich Materialien für diejenigen, die mit C # oder Java-Erfahrung kommen. Die folgenden Links können hilfreich sein, wenn Sie tiefer in F # einsteigen:
Es werden auch verschiedene andere Möglichkeiten beschrieben , um mit dem Lernen von F # zu beginnen .
Schließlich ist die F # Community sehr anfängerfreundlich. Bei Slack gibt es einen sehr aktiven Chat, der von der F # Software Foundation unterstützt wird, mit Anfängerräumen, an denen Sie frei teilnehmen können . Wir empfehlen Ihnen dringend, dies zu tun!
Vergessen Sie nicht, die Seite der russischsprachigen Community F # zu besuchen! Wenn Sie Fragen zum Erlernen einer Sprache haben, diskutieren wir diese gerne in Chatrooms:
Über Übersetzungsautoren
Übersetzt von @kleidemos
Übersetzungs- und redaktionelle Änderungen wurden durch die Bemühungen der russischsprachigen Community von F # -Entwicklern vorgenommen . Wir danken auch @schvepsss und @shwars für die Vorbereitung dieses Artikels zur Veröffentlichung.