Funktionales Denken. Teil 7

Wir setzen unsere Artikelserie zur funktionalen Programmierung in F # fort. Heute haben wir ein sehr interessantes Thema: die Definition von Funktionen. Lassen Sie uns unter anderem über anonyme Funktionen, Funktionen ohne Parameter, rekursive Funktionen, Kombinatoren und vieles mehr sprechen. Schau unter die Katze!




Funktionsdefinition


Wir wissen bereits, wie man reguläre Funktionen mit der "let" -Syntax erstellt:


let add xy = x + y 

In diesem Artikel werden einige andere Möglichkeiten zum Erstellen von Funktionen sowie Tipps zu deren Definition vorgestellt.


Anonyme Funktionen (Lambdas)


Wenn Sie mit Lambdas in anderen Sprachen vertraut sind, werden Ihnen die folgenden Absätze bekannt vorkommen. Anonyme Funktionen (oder „Lambda-Ausdrücke“) sind wie folgt definiert:


 fun parameter1 parameter2 etc -> expression 

Im Vergleich zu Lambdas aus C # gibt es zwei Unterschiede:


  • Lambdas sollten mit dem Schlüsselwort fun , das in C # nicht erforderlich ist
  • Es wird ein einzelner Pfeil verwendet -> anstelle von double => von C #.

Lambda-Definition der Additionsfunktion:


 let add = fun xy -> x + y 

Gleiche Funktion in traditioneller Form:


 let add xy = x + y 

Lambdas werden häufig in Form kleiner Ausdrücke verwendet oder wenn kein Wunsch besteht, eine separate Funktion für einen Ausdruck zu definieren. Wie Sie bereits gesehen haben, ist dies bei der Arbeit mit Listen keine Seltenheit.


 //    let add1 i = i + 1 [1..10] |> List.map add1 //        [1..10] |> List.map (fun i -> i + 1) 

Beachten Sie, dass um Lambdas herum Klammern verwendet werden müssen.


Lambdas werden auch verwendet, wenn eine deutlich andere Funktion benötigt wird. Zum Beispiel kann der zuvor diskutierte " adderGenerator ", den wir zuvor besprochen haben, mit Lambdas umgeschrieben werden.


 //   let adderGenerator x = (+) x //     let adderGenerator x = fun y -> x + y 

Die Lambda-Version ist etwas länger, macht aber sofort deutlich, dass eine Zwischenfunktion zurückgegeben wird.


Lambdas können verschachtelt werden. Ein weiteres Beispiel für eine adderGenerator Definition, diesmal nur für Lambdas.


 let adderGenerator = fun x -> (fun y -> x + y) 

Ist Ihnen klar, dass alle drei Definitionen gleichwertig sind?


 let adderGenerator1 xy = x + y let adderGenerator2 x = fun y -> x + y let adderGenerator3 = fun x -> (fun y -> x + y) 

Wenn nicht, lesen Sie das Kapitel über Curry erneut . Dies ist sehr wichtig für das Verständnis!


Mustervergleich


Wenn eine Funktion definiert ist, ist es möglich, Parameter explizit an sie zu übergeben, wie in den obigen Beispielen, aber es ist auch möglich, direkt im Parameterabschnitt mit einer Vorlage zu vergleichen. Mit anderen Worten, der Parameterabschnitt kann Muster (übereinstimmende Muster) und nicht nur Bezeichner enthalten!


Das folgende Beispiel zeigt die Verwendung von Mustern in einer Funktionsdefinition:


 type Name = {first:string; last:string} //    let bob = {first="bob"; last="smith"} //   //     let f1 name = //   let {first=f; last=l} = name //     printfn "first=%s; last=%s" fl //   let f2 {first=f; last=l} = //        printfn "first=%s; last=%s" fl //  f1 bob f2 bob 

Diese Art des Vergleichs kann nur stattfinden, wenn die Entsprechung immer entscheidbar ist. Beispielsweise können Sie Unionstypen und -listen auf diese Weise nicht abgleichen, da einige Fälle nicht abgeglichen werden können.


 let f3 (x::xs) = //       printfn "first element is=%A" x 

Der Compiler warnt vor unvollständigem Abgleich (eine leere Liste führt zu einem Laufzeitfehler am Eingang dieser Funktion).


Häufiger Fehler: Tupel vs. viele Parameter


Wenn Sie aus einer C-ähnlichen Sprache stammen, kann das als einziges Argument der Funktion verwendete Tupel schmerzhaft einer Multiparameterfunktion ähneln. Das ist aber nicht dasselbe! Wie ich bereits erwähnt habe, ist dies, wenn Sie ein Komma sehen, höchstwahrscheinlich ein Tupel. Parameter werden durch Leerzeichen getrennt.


Verwirrungsbeispiel:


 //      let addTwoParams xy = x + y //      -  let addTuple aTuple = let (x,y) = aTuple x + y //         //        let addConfusingTuple (x,y) = x + y 

  • Die erste Definition, " addTwoParams ", verwendet zwei Parameter, die durch ein Leerzeichen getrennt sind.
  • Die zweite Definition, " addTuple ", verwendet einen Parameter. Dieser Parameter bindet "x" und "y" aus dem Tupel und summiert sie.
  • Die dritte Definition, " addConfusingTuple ", verwendet einen Parameter wie " addTuple ", aber der Trick besteht darin, dass dieses Tupel entpackt (an das Muster angepasst) und als Teil der Parameterdefinition mithilfe des Mustervergleichs gebunden wird. Hinter den Kulissen geschieht alles genauso wie in addTuple .

Schauen wir uns die Signaturen an (schauen Sie sie sich immer an, wenn Sie sich bei etwas nicht sicher sind).


 val addTwoParams : int -> int -> int //   val addTuple : int * int -> int // tuple->int val addConfusingTuple : int * int -> int // tuple->int 

Und jetzt hier:


 // addTwoParams 1 2 // ok --      addTwoParams (1,2) // error -     // => error FS0001: This expression was expected to have type // int but here has type 'a * 'b 

Hier sehen wir einen Fehler im zweiten Aufruf.


Erstens behandelt der Compiler (1,2) als verallgemeinertes Tupel des Formulars ('a * 'b) , das er als ersten Parameter an addTwoParams zu übergeben addTwoParams . Danach beschwert er sich, dass der erwartete erste Parameter addTwoParams nicht int , sondern versucht wurde, ein Tupel zu übergeben.


Verwenden Sie ein Komma, um ein Tupel zu erstellen!


 addTuple (1,2) // ok addConfusingTuple (1,2) // ok let x = (1,2) addTuple x // ok let y = 1,2 //  , //  ! addTuple y // ok addConfusingTuple y // ok 

Und umgekehrt, wenn Sie mehrere Argumente an eine Funktion übergeben, die auf ein Tupel wartet, erhalten Sie auch einen unverständlichen Fehler.


 addConfusingTuple 1 2 // error --          // => error FS0003: This value is not a function and // cannot be applied 

Diesmal entschied der Compiler, dass addConfusingTuple nach addConfusingTuple zwei Argumenten als Curry verwendet werden sollte. Und der Eintrag " addConfusingTuple 1 " ist eine Teilanwendung und sollte eine Zwischenfunktion zurückgeben. Der Versuch, diese Zwischenfunktion mit dem Parameter "2" aufzurufen, löst einen Fehler aus, weil Es gibt keine Zwischenfunktion! Wir sehen den gleichen Fehler wie im Kapitel über Currying, in dem wir Probleme mit zu vielen Parametern besprochen haben.


Warum nicht Tupel als Parameter verwenden?


Die obige Diskussion der Tupel zeigt eine andere Möglichkeit, Funktionen mit vielen Parametern zu definieren: Anstatt sie separat zu übergeben, können alle Parameter zu einer Struktur zusammengefasst werden. Im folgenden Beispiel verwendet die Funktion einen einzelnen Parameter - ein Tupel aus drei Elementen.


 let f (x,y,z) = x + y * z //  - int * int * int -> int //  f (1,2,3) 

Es ist zu beachten, dass sich die Signatur von der Signatur einer Funktion mit drei Parametern unterscheidet. Es gibt nur einen Pfeil, einen Parameter und Sternchen, die auf das Tupel zeigen (int*int*int) .


Wann müssen Argumente mit separaten Parametern eingereicht werden und wann ein Tupel?


  • Wenn Tupel an sich bedeutsam sind. Beispielsweise sind dreifache Tupel für Operationen im dreidimensionalen Raum bequemer als drei separate Koordinaten.
  • Manchmal werden Tupel verwendet, um Daten, die zusammen gespeichert werden müssen, in einer einzigen Struktur zu kombinieren. Beispielsweise geben TryParse Methoden aus der .NET-Bibliothek das Ergebnis und eine boolesche Variable als Tupel zurück. Um jedoch eine große Menge verwandter Daten zu speichern, ist es besser, eine Klasse oder einen Datensatz ( Datensatz) zu definieren.

Sonderfall: .NET Library Tupel und Funktionen


Beim Aufrufen von .NET-Bibliotheken sind Kommas sehr häufig!


Sie alle akzeptieren Tupel und die Aufrufe sehen genauso aus wie in C #:


 //  System.String.Compare("a","b") //   System.String.Compare "a" "b" 

Der Grund dafür ist, dass die Funktionen von klassischem .NET nicht aktuell sind und nicht teilweise angewendet werden können. Alle Parameter müssen immer sofort übertragen werden, und der naheliegendste Weg ist die Verwendung eines Tupels.


Beachten Sie, dass diese Aufrufe nur wie das Übertragen von Tupeln aussehen. Dies ist jedoch ein Sonderfall. Sie können keine echten Tupel an solche Funktionen übergeben:


 let tuple = ("a","b") System.String.Compare tuple // error System.String.Compare "a","b" // error 

Wenn Sie .NET-Funktionen teilweise anwenden möchten, schreiben Sie einfach Wrapper darüber, wie zuvor oder wie unten gezeigt:


 //    let strCompare xy = System.String.Compare(x,y) //    let strCompareWithB = strCompare "B" //      ["A";"B";"C"] |> List.map strCompareWithB 

Anleitung zur Auswahl einzelner und gruppierter Parameter


Die Diskussion von Tupeln führt zu einem allgemeineren Thema: Wann sollten Parameter getrennt und wann gruppiert werden?


Sie sollten darauf achten, wie sich F # in dieser Hinsicht von C # unterscheidet. In C # werden immer alle Parameter übergeben, so dass diese Frage dort nicht einmal auftaucht! Aufgrund der teilweisen Anwendung in F # können nur einige der Parameter dargestellt werden. Daher muss zwischen dem Fall, in dem die Parameter kombiniert werden sollen, und dem Fall, in dem sie unabhängig sind, unterschieden werden.


Allgemeine Empfehlungen zum Strukturieren von Parametern beim Entwerfen eigener Funktionen.


  • Im allgemeinen Fall ist es immer besser, separate Parameter zu verwenden, anstatt eine Struktur zu übergeben, sei es ein Tupel oder ein Datensatz. Dies ermöglicht ein flexibleres Verhalten, beispielsweise eine teilweise Anwendung.
  • Wenn jedoch eine Gruppe von Parametern gleichzeitig übergeben werden muss, sollte eine Art Gruppierungsmechanismus verwendet werden.

Mit anderen Worten, wenn Sie eine Funktion entwickeln, fragen Sie sich: "Kann ich diesen Parameter separat angeben?" Wenn die Antwort Nein lautet, sollten die Parameter gruppiert werden.


Schauen wir uns einige Beispiele an:


 //     . //      ,       let add xy = x + y //         //      ,    let locateOnMap (xCoord,yCoord) = //  //      //      -     type CustomerName = {First:string; Last:string} let setCustomerName aCustomerName = //  let setCustomerName first last = //   //     //     //    ,     let setCustomerName myCredentials aName = // 

Stellen Sie schließlich sicher, dass die Reihenfolge der Parameter bei der Teilanwendung hilfreich ist (siehe Handbuch hier ). Warum habe ich aName in der letzten Funktion myCredentials vor aName ?


Funktionen ohne Parameter


Manchmal benötigen Sie möglicherweise eine Funktion, die keine Parameter akzeptiert. Zum Beispiel benötigen Sie die Funktion "Hallo Welt", die mehrfach aufgerufen werden kann. Wie im vorherigen Abschnitt gezeigt, funktioniert die naive Definition nicht.


 let sayHello = printfn "Hello World!" //      

Dies kann jedoch behoben werden, indem der Funktion ein Einheitsparameter hinzugefügt oder ein Lambda verwendet wird.


 let sayHello() = printfn "Hello World!" //  let sayHello = fun () -> printfn "Hello World!" //  

Danach sollte die Funktion immer mit dem Argument unit aufgerufen unit :


 //  sayHello() 

Was passiert ziemlich oft bei der Interaktion mit .NET-Bibliotheken:


 Console.ReadLine() System.Environment.GetCommandLineArgs() System.IO.Directory.GetCurrentDirectory() 

Denken Sie daran, rufen Sie sie mit unit !


Neue Operatoren definieren


Sie können Funktionen mit einem oder mehreren Operatorzeichen definieren (eine Liste der Zeichen finden Sie in der Dokumentation ):


 //  let (.*%) xy = x + y + 1 

Sie müssen Zeichen um Klammern setzen, um Funktionen zu definieren.


Operatoren, die mit * benötigen ein Leerzeichen zwischen der Klammer und * , weil in F # (* fungiert als Anfang eines Kommentars (wie /*...*/ in C #):


 let ( *+* ) xy = x + y + 1 

Einmal definiert, kann eine neue Funktion auf die übliche Weise verwendet werden, wenn sie in Klammern gesetzt ist:


 let result = (.*%) 2 3 

Wenn die Funktion mit zwei Parametern verwendet wird, können Sie den Infix-Operator-Datensatz ohne Klammern verwenden.


 let result = 2 .*% 3 

Sie können auch Präfixoperatoren definieren, die mit beginnen ! oder ~ (mit einigen Einschränkungen siehe Dokumentation )


 let (~%%) (s:string) = s.ToCharArray() // let result = %% "hello" 

In F # ist das Definieren von Anweisungen eine ziemlich häufige Operation, und viele Bibliotheken exportieren Anweisungen mit Namen wie >=> und <*> .


Punktfreier Stil


Wir haben bereits viele Beispiele für Funktionen gesehen, denen die neuesten Parameter fehlten, um das Chaos zu verringern. Dieser Stil wird als punktfreier Stil oder stillschweigende Programmierung bezeichnet .


Hier einige Beispiele:


 let add xy = x + y //  let add x = (+) x // point free let add1Times2 x = (x + 1) * 2 //  let add1Times2 = (+) 1 >> (*) 2 // point free let sum list = List.reduce (fun sum e -> sum+e) list //  let sum = List.reduce (+) // point free 

Dieser Stil hat seine Vor- und Nachteile.


Einer der Vorteile besteht darin, dass der Schwerpunkt auf der Zusammensetzung von Funktionen höherer Ordnung liegt, anstatt sich mit Objekten auf niedriger Ebene zu beschäftigen. Zum Beispiel ist " (+) 1 >> (*) 2 " eine explizite Addition, gefolgt von einer Multiplikation. Und " List.reduce (+) " macht deutlich, dass der Additionsvorgang unabhängig von den List.reduce (+) wichtig ist.


Ein sinnloser Stil ermöglicht es Ihnen, sich auf den grundlegenden Algorithmus zu konzentrieren und gemeinsame Merkmale im Code zu identifizieren. Die oben verwendete " reduce " -Funktion ist ein gutes Beispiel. Dieses Thema wird in einer geplanten Reihe zur Listenverarbeitung behandelt.


Andererseits kann eine übermäßige Verwendung eines solchen Stils den Code verdecken. Explizite Parameter dienen als Dokumentation und ihre Namen (z. B. "Liste") erleichtern das Verständnis der Funktionsweise der Funktion.


Wie alles in der Programmierung ist die beste Empfehlung, den Ansatz zu bevorzugen, der die größte Klarheit bietet.


Kombinatoren


" Kombinatoren " werden Funktionen genannt, deren Ergebnis nur von ihren Parametern abhängt. Dies bedeutet, dass keine Abhängigkeit von der Außenwelt besteht und insbesondere keine anderen Funktionen oder globalen Werte diese beeinflussen können.


In der Praxis bedeutet dies, dass kombinatorische Funktionen durch eine Kombination ihrer Parameter auf verschiedene Weise begrenzt werden.


Wir haben bereits mehrere Kombinatoren gesehen: einen Rohr- und einen Kompositionsoperator. Wenn Sie sich ihre Definitionen ansehen, ist es klar, dass sie die Parameter nur auf verschiedene Arten neu anordnen.


 let (|>) xf = fx //  pipe let (<|) fx = fx //  pipe let (>>) fgx = g (fx) //   let (<<) gfx = g (fx) //   

Andererseits sind Funktionen wie "printf", obwohl primitiv, keine Kombinatoren, da sie von der Außenwelt (E / A) abhängig sind.


Kombinatorische Vögel


Kombinatoren sind die Grundlage eines ganzen Abschnitts der Logik (natürlich "kombinatorische Logik" genannt), der viele Jahre vor Computern und Programmiersprachen erfunden wurde. Die kombinatorische Logik hat einen sehr großen Einfluss auf die funktionale Programmierung.


Um mehr über Kombinatoren und kombinatorische Logik zu erfahren, empfehle ich Raymond Smullyans Buch "To Mock a Mockingbird". Darin erklärt er andere Kombinatoren und gibt ihnen phantasievoll Vogelnamen . Hier sind einige Beispiele für Standardkombinatoren und ihre Vogelnamen:


 let I x = x //  ,  Idiot bird let K xy = x // the Kestrel let M x = x >> x // the Mockingbird let T xy = yx // the Thrush ( !) let Q xyz = y (xz) // the Queer bird ( !) let S xyz = xz (yz) // The Starling //   ... let rec Y fx = f (Y f) x // Y-,  Sage bird 

Die Buchstabennamen sind Standard, sodass Sie den K-Kombinator an jeden weiterleiten können, der mit dieser Terminologie vertraut ist.


Es stellt sich heraus, dass viele gängige Programmiermuster durch diese Standardkombinatoren dargestellt werden können. Zum Beispiel ist Turmfalke ein häufiges Muster in der fließenden Benutzeroberfläche, in dem Sie etwas tun, aber das ursprüngliche Objekt zurückgeben. Thrush ist eine Pipe, Queer ist eine direkte Komposition und der Y-Kombinator leistet hervorragende Arbeit bei der Erstellung rekursiver Funktionen.


Tatsächlich gibt es einen bekannten Satz, dass jede berechenbare Funktion mit nur zwei grundlegenden Kombinatoren, Turmfalke und Starling, konstruiert werden kann.


Kombinatorische Bibliotheken


Kombinatorische Bibliotheken sind Bibliotheken, die viele kombinatorische Funktionen exportieren, die gemeinsam genutzt werden sollen. Ein Benutzer einer solchen Bibliothek kann Funktionen leicht miteinander kombinieren, um noch größere und komplexere Funktionen wie Würfel zu erhalten.


Eine gut gestaltete Combiner-Bibliothek ermöglicht es Ihnen, sich auf Funktionen auf hoher Ebene zu konzentrieren und "Rauschen" auf niedriger Ebene zu verbergen. Wir haben ihre Leistungsfähigkeit bereits in mehreren Beispielen in der Reihe "Warum F # verwenden" gesehen, und das List Modul ist voll von solchen Funktionen. " fold " und " map " sind auch Kombinatoren, wenn Sie darüber nachdenken.


Ein weiterer Vorteil von Kombinatoren ist, dass sie die sicherste Art von Funktion sind. Weil Sie haben keine Abhängigkeiten von der Außenwelt und können sich nicht ändern, wenn sich die globale Umgebung ändert. Eine Funktion, die einen globalen Wert liest oder Bibliotheksfunktionen verwendet, kann zwischen Aufrufen unterbrochen werden, wenn sich der Kontext ändert. Dies wird Kombinatoren niemals passieren.


In F # stehen Kombinatorbibliotheken zum Parsen (FParsec), Erstellen von HTML, Testen von Frameworks usw. zur Verfügung. Wir werden später in der nächsten Reihe Kombinatoren diskutieren und verwenden.


Rekursive Funktionen


Oft muss sich eine Funktion von ihrem Körper aus auf sich selbst beziehen. Ein klassisches Beispiel ist die Fibonacci-Funktion.


 let fib i = match i with | 1 -> 1 | 2 -> 1 | n -> fib(n-1) + fib(n-2) 

Leider kann diese Funktion nicht kompilieren:


 error FS0039: The value or constructor 'fib' is not defined 

Sie müssen dem Compiler mit dem Schlüsselwort rec mitteilen, dass dies eine rekursive Funktion ist.


 let rec fib i = match i with | 1 -> 1 | 2 -> 1 | n -> fib(n-1) + fib(n-2) 

Rekursive Funktionen und Datenstrukturen sind in der funktionalen Programmierung sehr verbreitet, und ich hoffe, dass ich später eine ganze Reihe diesem Thema widmen kann.


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/de433398/


All Articles