Funktionales Denken. Teil 4

Nach einem kurzen Exkurs in die Grundtypen können wir wieder zu den Funktionen zurückkehren. Insbesondere zum zuvor erwähnten Rätsel: Wenn eine mathematische Funktion nur einen Parameter annehmen kann, wie kann es dann eine Funktion in F # geben, die eine größere Anzahl von Parametern akzeptiert? Weitere Details unter dem Schnitt!




Die Antwort ist ganz einfach: Eine Funktion mit mehreren Parametern wird als eine Reihe neuer Funktionen umgeschrieben, von denen jede nur einen Parameter akzeptiert. Der Compiler führt diese Operation automatisch aus und wird zu Ehren von Haskell Curry, einem Mathematiker, der die Entwicklung der funktionalen Programmierung maßgeblich beeinflusst hat, als " Currying " bezeichnet.


Um zu sehen, wie das Currying in der Praxis funktioniert, verwenden wir ein einfaches Codebeispiel, das zwei Zahlen druckt:


//   let printTwoParameters xy = printfn "x=%iy=%i" xy 

Tatsächlich schreibt der Compiler es in ungefähr der folgenden Form neu:


 //    let printTwoParameters x = //    let subFunction y = printfn "x=%iy=%i" xy //  ,    subFunction //   

Betrachten Sie diesen Prozess genauer:


  1. Eine Funktion mit dem Namen " printTwoParameters " wird printTwoParameters , akzeptiert jedoch nur einen Parameter: "x".
  2. Darin wird eine lokale Funktion erstellt, die auch nur einen Parameter akzeptiert: "y". Beachten Sie, dass die lokale Funktion den Parameter "x" verwendet, x jedoch nicht als Argument an sie übergeben wird. "x" befindet sich in einem solchen Bereich, dass eine verschachtelte Funktion es sehen und verwenden kann, ohne es übergeben zu müssen.
  3. Schließlich wird die neu erstellte lokale Funktion zurückgegeben.
  4. Die zurückgegebene Funktion wird dann auf das Argument "y" angewendet. Der Parameter "x" ist darin geschlossen, so dass die zurückgegebene Funktion nur den Parameter "y" benötigt, um ihre Logik zu vervollständigen.

Durch das Umschreiben von Funktionen auf diese Weise stellt der Compiler sicher, dass jede Funktion je nach Bedarf nur einen Parameter akzeptiert. Wenn Sie also " printTwoParameters " verwenden, denken Sie möglicherweise, dass dies eine Funktion mit zwei Parametern ist, aber tatsächlich wird eine Funktion mit nur einem Parameter verwendet. Sie können dies überprüfen, indem Sie nur ein Argument anstelle von zwei übergeben:


 //     printTwoParameters 1 //    val it : (int -> unit) = <fun:printTwoParameters@286-3> 

Wenn wir es mit einem Argument berechnen, erhalten wir keinen Fehler - die Funktion wird zurückgegeben.


printTwoParameters passiert also tatsächlich, wenn printTwoParameters mit zwei Argumenten printTwoParameters wird:


  • printTwoParameters wird mit dem ersten Argument (x) printTwoParameters
  • printTwoParameters gibt eine neue Funktion zurück, in der "x" geschlossen ist.
  • Dann wird eine neue Funktion mit dem zweiten Argument (y) aufgerufen.

Hier ist ein Beispiel für schrittweise und normale Versionen:


 //   let x = 6 let y = 99 let intermediateFn = printTwoParameters x //  -  // x   let result = intermediateFn y //     let result = (printTwoParameters x) y //   let result = printTwoParameters xy 

Hier ist ein weiteres Beispiel:


 //  let addTwoParameters xy = x + y //   let addTwoParameters x = //   ! let subFunction y = x + y //      subFunction //   //       let x = 6 let y = 99 let intermediateFn = addTwoParameters x //  -  // x   let result = intermediateFn y //   let result = addTwoParameters xy 

Wiederum ist eine "Funktion mit zwei Parametern" tatsächlich eine Funktion mit einem Parameter, die eine Zwischenfunktion zurückgibt.


Aber warte, was ist mit dem Operator + ? Ist dies eine binäre Operation, die zwei Parameter annehmen muss? Nein, es ist auch Curry, wie andere Funktionen. Dies ist eine Funktion namens " + ", die einen Parameter verwendet und eine neue Zwischenfunktion zurückgibt, genau wie oben bei addTwoParameters .


Wenn wir den Ausdruck x+y schreiben, ordnet der Compiler den Code so um, dass das Infix in (+) xy konvertiert wird. Dies ist eine Funktion mit dem Namen + , die zwei Parameter akzeptiert. Beachten Sie, dass die Funktion „+“ Klammern benötigt, um anzugeben, dass sie als reguläre Funktion und nicht als Infix-Operator verwendet wird.


Schließlich wird eine Funktion mit zwei Parametern, + , wie jede andere Funktion mit zwei Parametern behandelt.


 //         let x = 6 let y = 99 let intermediateFn = (+) x //   ""  ""   let result = intermediateFn y //        let result = (+) xy //       let result = x + y 

Und ja, dies funktioniert für alle anderen Operatoren und integrierten Funktionen wie printf .


 //    let result = 3 * 5 //    - let intermediateFn = (*) 3 //  ""  3   let result = intermediateFn 5 //    printfn let result = printfn "x=%iy=%i" 3 5 // printfn   - let intermediateFn = printfn "x=%iy=%i" 3 // "3"   let result = intermediateFn 5 

Curry-Funktionssignaturen


Jetzt, da wir wissen, wie Curry-Funktionen funktionieren, ist es interessant zu wissen, wie ihre Signaturen aussehen werden.


Zurück zum ersten Beispiel, " printTwoParameter ", haben wir gesehen, dass die Funktion ein Argument verwendet und eine Zwischenfunktion zurückgegeben hat. Die Zwischenfunktion nahm auch ein Argument und gab nichts zurück (d. H. unit ). Daher war die Zwischenfunktion vom Typ int->unit . Mit anderen Worten, domain printTwoParameters ist int und range ist int->unit . Wenn wir alles zusammenfügen, sehen wir die endgültige Unterschrift:


 val printTwoParameters : int -> (int -> unit) 

Wenn Sie eine explizit Curry-Implementierung berechnen, sehen Sie die Klammern in der Signatur. Wenn Sie jedoch eine normale, implizit Curry-Implementierung berechnen, gibt es keine Klammern:


 val printTwoParameters : int -> int -> unit 

Klammern sind optional. Sie können jedoch im Kopf dargestellt werden, um die Wahrnehmung von Funktionssignaturen zu vereinfachen.


Und was ist der Unterschied zwischen einer Funktion, die eine Zwischenfunktion zurückgibt, und einer regulären Funktion mit zwei Parametern?


Hier ist eine Funktion mit einem Parameter, die eine andere Funktion zurückgibt:


 let add1Param x = (+) x // signature is = int -> (int -> int) 

Und hier ist eine Funktion mit zwei Parametern, die einen einfachen Wert zurückgibt:


 let add2Params xy = (+) xy // signature is = int -> int -> int 

Ihre Signaturen unterscheiden sich geringfügig, aber im praktischen Sinne gibt es keinen großen Unterschied zwischen ihnen, außer dass die zweite Funktion automatisch ausgeführt wird.


Funktioniert mit mehr als zwei Parametern


Wie funktioniert das Currying für Funktionen mit mehr als zwei Parametern? Auf die gleiche Weise: Für jeden Parameter außer dem letzten gibt die Funktion eine Zwischenfunktion zurück, die den vorherigen Parameter schließt.


Betrachten Sie dieses schwierige Beispiel. Ich habe explizit Parametertypen deklariert, aber die Funktion macht nichts.


 let multiParamFn (p1:int)(p2:bool)(p3:string)(p4:float)= () //   let intermediateFn1 = multiParamFn 42 // multoParamFn  int   (bool -> string -> float -> unit) // intermediateFn1  bool //   (string -> float -> unit) let intermediateFn2 = intermediateFn1 false // intermediateFn2  string //   (float -> unit) let intermediateFn3 = intermediateFn2 "hello" // intermediateFn3 float //     (unit) let finalResult = intermediateFn3 3.141 

Unterschrift der gesamten Funktion:


 val multiParamFn : int -> bool -> string -> float -> unit 

und Signaturen von Zwischenfunktionen:


 val intermediateFn1 : (bool -> string -> float -> unit) val intermediateFn2 : (string -> float -> unit) val intermediateFn3 : (float -> unit) val finalResult : unit = () 

Die Signatur der Funktion kann Ihnen sagen, wie viele Parameter die Funktion benötigt: Zählen Sie einfach die Anzahl der Pfeile außerhalb der Klammern. Wenn die Funktion eine andere Funktion akzeptiert oder zurückgibt, werden mehr Pfeile angezeigt, die sich jedoch in Klammern befinden und ignoriert werden können. Hier einige Beispiele:


 int->int->int // 2  int  int string->bool->int //   string,  - bool, //  int int->string->bool->unit //   (int,string,bool) //    (unit) (int->string)->int //   ,  // ( int  string) //   int (int->string)->(int->bool) //   (int  string) //   (int  bool) 

Schwierigkeiten mit mehreren Parametern


Bis Sie die Logik hinter dem Curry verstehen, werden einige unerwartete Ergebnisse erzielt. Denken Sie daran, dass Sie keine Fehlermeldung erhalten, wenn Sie die Funktion mit weniger Argumenten als erwartet ausführen. Stattdessen erhalten Sie eine teilweise angewendete Funktion. Wenn Sie dann die teilweise angewendete Funktion in dem Kontext verwenden, in dem der Wert erwartet wird, kann der Compiler einen undurchsichtigen Fehler erhalten.


Stellen Sie sich eine Funktion vor, die auf den ersten Blick harmlos ist:


 //   let printHello() = printfn "hello" 

Was wird Ihrer Meinung nach passieren, wenn Sie es wie unten gezeigt aufrufen? Wird "Hallo" auf der Konsole gedruckt? Versuchen Sie vor der Ausführung zu raten. Hinweis: Sehen Sie sich die Signatur der Funktion an.


 //   printHello 

Entgegen den Erwartungen wird es keinen Anruf geben. Die ursprüngliche Funktion erwartet unit als ein Argument, das nicht übergeben wurde. Daher wurde eine teilweise angewendete Funktion erhalten (in diesem Fall ohne Argumente).


Was ist mit diesem Fall? Wird es kompiliert?


 let addXY xy = printfn "x=%iy=%i" x x + y 

Wenn Sie es ausführen, beschwert sich der Compiler über die Zeile mit printfn .


 printfn "x=%iy=%i" x //^^^^^^^^^^^^^^^^^^^^^ //warning FS0193: This expression is a function value, ie is missing //arguments. Its type is ^a -> unit. 

Wenn Sie kein Verständnis für Curry haben, kann diese Nachricht sehr kryptisch sein. Tatsache ist, dass alle Ausdrücke, die separat ausgewertet werden (dh nicht als Rückgabewert verwendet werden oder mittels "let" an etwas gebunden sind) , im unit ausgewertet werden müssen . In diesem Fall wird es nicht im unit berechnet, sondern gibt eine Funktion zurück. Dies ist ein langer Weg zu sagen, dass printfn ein Argument fehlt.


In den meisten Fällen treten solche Fehler bei der Interaktion mit einer Bibliothek aus der .NET-Welt auf. Beispielsweise muss die Readline Methode der TextReader Klasse einen unit annehmen. Sie können dies oft vergessen und keine Klammern setzen. In diesem Fall können Sie zum Zeitpunkt des „Aufrufs“ keinen Compilerfehler erhalten. Dieser wird jedoch angezeigt, wenn Sie versuchen, das Ergebnis als Zeichenfolge zu interpretieren.


 let reader = new System.IO.StringReader("hello"); let line1 = reader.ReadLine // ,    printfn "The line is %s" line1 //    // ==> error FS0001: This expression was expected to have // type string but here has type unit -> string let line2 = reader.ReadLine() // printfn "The line is %s" line2 //   

Im obigen Code ist line1 nur ein Zeiger oder Delegat an die Readline Methode, nicht wie erwartet eine Zeichenfolge. Wenn Sie () in reader.ReadLine() wird die Funktion tatsächlich aufgerufen.


Zu viele Optionen


Sie können gleichermaßen kryptische Nachrichten erhalten, wenn Sie zu viele Parameter an eine Funktion übergeben. Einige Beispiele für die Übergabe zu vieler Parameter an printf :


 printfn "hello" 42 // ==> error FS0001: This expression was expected to have // type 'a -> 'b but here has type unit printfn "hello %i" 42 43 // ==> Error FS0001: Type mismatch. Expecting a 'a -> 'b -> 'c // but given a 'a -> unit printfn "hello %i %i" 42 43 44 // ==> Error FS0001: Type mismatch. Expecting a 'a->'b->'c->'d // but given a 'a -> 'b -> unit 

Im letzteren Fall meldet der Compiler beispielsweise, dass eine Formatzeichenfolge mit drei Parametern erwartet wird (Signatur 'a -> 'b -> 'c -> 'd hat drei Parameter), aber stattdessen wird eine Zeichenfolge mit zwei Parametern empfangen (für Signatur 'a -> 'b -> unit zwei Parameter).


In den Fällen, in denen printf nicht verwendet wird, bedeutet das Übergeben einer großen Anzahl von Parametern häufig, dass in einer bestimmten Phase der Berechnung ein einfacher Wert erhalten wurde, für den der Parameter ausprobiert wird. Der Compiler wird erneut senden, dass ein einfacher Wert keine Funktion ist.


 let add1 x = x + 1 let x = add1 2 3 // ==> error FS0003: This value is not a function // and cannot be applied 

Wenn wir den allgemeinen Aufruf wie zuvor in eine Reihe expliziter Zwischenfunktionen aufteilen, können wir sehen, was genau falsch läuft.


 let add1 x = x + 1 let intermediateFn = add1 2 //   let x = intermediateFn 3 //intermediateFn  ! // ==> error FS0003: This value is not a function // and cannot be applied 

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.

Source: https://habr.com/ru/post/de430620/


All Articles