Funktionales Denken. Teil 10

Können Sie sich vorstellen, dass dies der zehnte Teil des Zyklus ist? Obwohl sich die Erzählung zuvor auf einen rein funktionalen Stil konzentriert hatte, ist es manchmal zweckmäßig, zu einem objektorientierten Stil zu wechseln. Eines der Hauptmerkmale eines objektorientierten Stils ist die Möglichkeit, Funktionen an eine Klasse anzuhängen und über einen Punkt auf die Klasse zuzugreifen, um das gewünschte Verhalten zu erzielen.






In F # ist dies mit einer Funktion namens "Typerweiterungen" möglich. Jeder F # -Typ, nicht nur eine Klasse, kann angehängte Funktionen haben.


Hier ist ein Beispiel für das Anhängen einer Funktion an einen Datensatztyp.


module Person = type T = {First:string; Last:string} with // -,     member this.FullName = this.First + " " + this.Last //  let create first last = {First=first; Last=last} let person = Person.create "John" "Doe" let fullname = person.FullName 

Wichtige Punkte, auf die Sie achten sollten:


  • Das Schlüsselwort with gibt den Anfang einer Mitgliederliste an.
  • Das Schlüsselwort member gibt an, dass die Funktion ein Mitglied ist (d. H. Eine Methode).
  • Das Wort this ist die Bezeichnung des Objekts, für das diese Methode aufgerufen wird (auch als "Selbstkennung" bezeichnet). Dieses Wort ist das Präfix des Funktionsnamens. Innerhalb der Funktion können Sie damit auf die aktuelle Instanz verweisen. Es gibt keine Anforderungen für Wörter, die als Selbstkennung verwendet werden. Es reicht aus, dass sie stabil sind. Sie können this , sich self , me oder jedes andere Wort verwenden, das normalerweise als Referenz für sich selbst verwendet wird.

Es ist nicht erforderlich, ein Mitglied zusammen mit einer Typdeklaration hinzuzufügen. Sie können es später jederzeit im selben Modul hinzufügen:


 module Person = type T = {First:string; Last:string} with // ,     member this.FullName = this.First + " " + this.Last //  let create first last = {First=first; Last=last} //  ,   type T with member this.SortableName = this.Last + ", " + this.First let person = Person.create "John" "Doe" let fullname = person.FullName let sortableName = person.SortableName 

Diese Beispiele veranschaulichen den Aufruf von "intrinsischen Erweiterungen". Sie werden zu einem Typ kompiliert und sind überall dort verfügbar, wo der Typ verwendet wird. Sie werden auch bei Verwendung der Reflexion angezeigt.


Mit internen Erweiterungen können Sie eine Typdefinition sogar in mehrere Dateien aufteilen, sofern alle Komponenten denselben Namespace verwenden und in einer Assembly kompiliert werden. Wie bei Teilklassen in C # kann dies nützlich sein, um generierten und handgeschriebenen Code zu trennen.


Optionale Erweiterungen


Eine Alternative besteht darin, ein zusätzliches Mitglied aus einem völlig anderen Modul hinzuzufügen. Sie werden als "optionale Erweiterungen" bezeichnet. Sie werden nicht innerhalb der Klasse kompiliert und erfordern ein anderes Bereichsmodul, um mit ihnen zu arbeiten (dieses Verhalten ähnelt Erweiterungsmethoden aus C #).


Lassen Sie zum Beispiel einen Person definieren:


 module Person = type T = {First:string; Last:string} with // ,     member this.FullName = this.First + " " + this.Last //  let create first last = {First=first; Last=last} //   ,   type T with member this.SortableName = this.Last + ", " + this.First 

Das folgende Beispiel zeigt, wie Sie die Erweiterung UppercaseName in einem anderen Modul hinzufügen:


 //    module PersonExtensions = type Person.T with member this.UppercaseName = this.FullName.ToUpper() 

Jetzt können Sie diese Erweiterung ausprobieren:


 let person = Person.create "John" "Doe" let uppercaseName = person.UppercaseName 

Ups, wir bekommen einen Fehler. Es ist passiert, weil PersonExtensions nicht im Geltungsbereich liegt. Wie in C # müssen Sie Erweiterungen in den Bereich eingeben, um sie verwenden zu können.


Sobald wir dies tun, wird es funktionieren:


 //    ! open PersonExtensions let person = Person.create "John" "Doe" let uppercaseName = person.UppercaseName 

Systemtyp-Erweiterungen


Sie können Typen auch aus .NET-Bibliotheken erweitern. Beachten Sie jedoch, dass Sie beim Erweitern eines Typs dessen tatsächlichen Namen und keinen Alias ​​verwenden müssen.


Wenn Sie beispielsweise versuchen, int zu erweitern, funktioniert nichts, weil int kein gültiger Name für den Typ:


 type int with member this.IsEven = this % 2 = 0 

Verwenden System.Int32 stattdessen System.Int32 :


 type System.Int32 with member this.IsEven = this % 2 = 0 let i = 20 if i.IsEven then printfn "'%i' is even" i 

Statische Mitglieder


Sie können statische Elementfunktionen erstellen, indem Sie:


  • static hinzufügen
  • Entfernen Sie this Tag

 module Person = type T = {First:string; Last:string} with // ,     member this.FullName = this.First + " " + this.Last //   static member Create first last = {First=first; Last=last} let person = Person.T.Create "John" "Doe" let fullname = person.FullName 

Sie können statische Elemente für Systemtypen erstellen:


 type System.Int32 with static member IsOdd x = x % 2 = 1 type System.Double with static member Pi = 3.141 let result = System.Int32.IsOdd 20 let pi = System.Double.Pi 

Vorhandene Funktionen anhängen


Ein sehr häufiges Muster ist das Anhängen vorhandener unabhängiger Funktionen an einen Typ. Es bietet mehrere Vorteile:


  • Während der Entwicklung können Sie unabhängige Funktionen deklarieren, die auf andere unabhängige Funktionen verweisen. Dies vereinfacht die Entwicklung, da die Typinferenz bei einem funktionalen Stil viel besser funktioniert als bei einem objektorientierten ("Punkt zu Punkt").
  • Einige Schlüsselfunktionen können jedoch an einen Typ angehängt werden. Auf diese Weise können Benutzer auswählen, welcher der Stile verwendet werden soll - funktional oder objektorientiert.

Ein Beispiel für eine solche Lösung ist eine Funktion aus der F # -Bibliothek, die die Länge der Liste berechnet. Sie können eine unabhängige Funktion aus dem List Modul verwenden oder als Instanzmethode aufrufen.


 let list = [1..10] //   let len1 = List.length list // -  let len2 = list.Length 

Im folgenden Beispiel hat der Typ zunächst keine Mitglieder, dann werden mehrere Funktionen definiert und schließlich wird die Funktion fullName an den Typ angehängt.


 module Person = // ,     type T = {First:string; Last:string} //  let create first last = {First=first; Last=last} //   let fullName {First=first; Last=last} = first + " " + last //       type T with member this.FullName = fullName this let person = Person.create "John" "Doe" let fullname = Person.fullName person //  let fullname2 = person.FullName //  

Die fullName Funktion fullName verfügt über einen Parameter, person . Das angehängte Mitglied erhält den Parameter vom Self-Link.


Hinzufügen vorhandener Funktionen mit mehreren Parametern


Es gibt noch eine weitere nette Funktion. Wenn eine zuvor definierte Funktion mehrere Parameter akzeptiert, müssen Sie beim Anhängen an einen Typ nicht alle diese Parameter erneut auflisten. Es reicht aus, this Parameter zuerst anzugeben.


Im folgenden Beispiel verfügt die Funktion hasSameFirstAndLastName über drei Parameter. Beim Anbringen reicht es jedoch aus, nur eine zu erwähnen!


 module Person = //    type T = {First:string; Last:string} //  let create first last = {First=first; Last=last} //   let hasSameFirstAndLastName (person:T) otherFirst otherLast = person.First = otherFirst && person.Last = otherLast //      type T with member this.HasSameFirstAndLastName = hasSameFirstAndLastName this let person = Person.create "John" "Doe" let result1 = Person.hasSameFirstAndLastName person "bob" "smith" //  let result2 = person.HasSameFirstAndLastName "bob" "smith" //  

Warum funktioniert es? Hinweis: Denken Sie an Curry und teilweise Verwendung!


Tupelmethoden


Wenn wir Methoden mit mehr als einem Parameter haben, müssen Sie eine Entscheidung treffen:


  • Wir können das Standardformular (Curry) verwenden, bei dem die Parameter durch Leerzeichen getrennt sind und eine teilweise Anwendung unterstützt wird.
  • oder wir können alle Parameter gleichzeitig in Form eines durch Kommas getrennten Tupels übergeben.

Die Curryform ist funktionaler, während die Tupelform objektorientierter ist.


Ein Tupelformular wird auch verwendet, um mit F # mit Standard-.NET-Bibliotheken zu interagieren. Sie sollten diesen Ansatz daher genauer betrachten.


Unsere Teststelle wird ein Product mit zwei Methoden sein, von denen jede mit einer der oben beschriebenen Methoden implementiert wird. Die TupleTotal CurriedTotal und TupleTotal machen dasselbe: Sie berechnen die Gesamtkosten des Produkts für eine bestimmte Menge und einen bestimmten Rabatt.


 type Product = {SKU:string; Price: float} with //   member this.CurriedTotal qty discount = (this.Price * float qty) - discount //   member this.TupleTotal(qty,discount) = (this.Price * float qty) - discount 

Testcode:


 let product = {SKU="ABC"; Price=2.0} let total1 = product.CurriedTotal 10 1.0 let total2 = product.TupleTotal(10,1.0) 

Bisher gibt es keinen großen Unterschied.


Wir wissen jedoch, dass die Curry-Version teilweise angewendet werden kann:


 let totalFor10 = product.CurriedTotal 10 let discounts = [1.0..5.0] let totalForDifferentDiscounts = discounts |> List.map totalFor10 

Andererseits ist die Tupelversion zu etwas fähig, das nicht Curry werden kann, nämlich:


  • Benannte Parameter
  • Optionale Parameter
  • Überlastung

Benannte Parameter mit Parametern in Form eines Tupels


Der Tupel-Ansatz unterstützt benannte Parameter:


 let product = {SKU="ABC"; Price=2.0} let total3 = product.TupleTotal(qty=10,discount=1.0) let total4 = product.TupleTotal(discount=1.0, qty=10) 

Wie Sie sehen, können Sie auf diese Weise die Reihenfolge der Argumente ändern, indem Sie Namen explizit angeben.


Achtung: Wenn nur einige der Parameter Namen haben, sollten diese Parameter immer am Ende stehen.


Optionale Parameter mit Parametern in Form eines Tupels


Bei Methoden mit Parametern in Form eines Tupels können Sie Parameter als optional markieren, indem Sie das Präfix in Form eines Fragezeichens vor dem Parameternamen verwenden.


  • Wenn der Parameter eingestellt ist, wird ein Some value an die Funktion übergeben
  • Sonst wird None kommen

Ein Beispiel:


 type Product = {SKU:string; Price: float} with //   member this.TupleTotal2(qty,?discount) = let extPrice = this.Price * float qty match discount with | None -> extPrice | Some discount -> extPrice - discount 

Und der Test:


 let product = {SKU="ABC"; Price=2.0} //    let total1 = product.TupleTotal2(10) //   let total2 = product.TupleTotal2(10,1.0) 

Das explizite Überprüfen auf None und Some kann mühsam sein, aber es gibt eine elegantere Lösung für die Behandlung optionaler Parameter.


Es gibt eine defaultArg Funktion, die einen Parameternamen als erstes Argument und einen Standardwert als zweites Argument verwendet. Wenn der Parameter eingestellt ist, wird der entsprechende Wert zurückgegeben, andernfalls der Standardwert.


Der gleiche Code mit defaulArg :


 type Product = {SKU:string; Price: float} with //   member this.TupleTotal2(qty,?discount) = let extPrice = this.Price * float qty let discount = defaultArg discount 0.0 extPrice - discount 

Methodenüberladung


In C # können Sie mehrere Methoden mit demselben Namen erstellen, die sich in ihrer Signatur unterscheiden (z. B. verschiedene Arten von Parametern und / oder deren Anzahl).


In einem rein funktionalen Modell ist dies nicht sinnvoll - die Funktion arbeitet mit einem bestimmten Argumenttyp (Domäne) und einem bestimmten Rückgabewert (Bereich). Dieselbe Funktion kann nicht mit anderen Domänen und Bereichen interagieren.


F # unterstützt jedoch das Überladen von Methoden, jedoch nur für Methoden (die an Typen angehängt sind) und nur für Methoden, die in einem Tupelstil geschrieben sind.


Hier ist ein Beispiel mit einer anderen Variante der TupleTotal Methode!


 type Product = {SKU:string; Price: float} with //   member this.TupleTotal3(qty) = printfn "using non-discount method" this.Price * float qty //   member this.TupleTotal3(qty, discount) = printfn "using discount method" (this.Price * float qty) - discount 

In der Regel schwört der F # -Compiler, dass es zwei Methoden mit demselben Namen gibt, aber in diesem Fall ist dies akzeptabel, weil Sie werden in Tupelnotation deklariert und ihre Signaturen sind unterschiedlich. (Um zu verdeutlichen, welche Methode aufgerufen wird, habe ich kleine Nachrichten zum Debuggen hinzugefügt.)


Anwendungsbeispiel:


 let product = {SKU="ABC"; Price=2.0} //    let total1 = product.TupleTotal3(10) //   let total2 = product.TupleTotal3(10,1.0) 

Hey! Nicht so schnell ... Die Nachteile der Verwendung von Methoden


Aus einer objektorientierten Welt kommend, können Sie versucht sein, überall Methoden anzuwenden, weil es etwas Vertrautes ist. Aber du solltest vorsichtig sein, weil Sie haben eine Reihe schwerwiegender Nachteile:


  • Methoden funktionieren nicht gut mit Typinferenz
  • Methoden funktionieren mit Funktionen höherer Ordnung nicht gut

In der Tat können Sie durch den Missbrauch von Methoden die mächtigsten und nützlichsten Aspekte der Programmierung in F # übersehen.


Mal sehen, was ich meine.


Methoden interagieren schlecht mit Typinferenz


Kehren wir zu dem Beispiel mit Person , in dem dieselbe Logik in einer unabhängigen Funktion und in einer Methode implementiert wurde:


 module Person = //    type T = {First:string; Last:string} //  let create first last = {First=first; Last=last} //   let fullName {First=first; Last=last} = first + " " + last // - type T with member this.FullName = fullName this 

Nun wollen wir sehen, wie gut die Typinferenz mit jeder der Methoden funktioniert. Angenommen, ich möchte den vollständigen Namen einer Person drucken, dann definiere printFullName Funktion printFullName , die die person als Parameter verwendet.


Code mit einer vom Modul unabhängigen Funktion:


 open Person //    let printFullName person = printfn "Name is %s" (fullName person) //    // val printFullName : Person.T -> unit 

Es wird ohne Probleme kompiliert und die Typinferenz identifiziert den Parameter korrekt als Person .


Versuchen Sie nun die Version durch den Punkt:


 open Person //    " " let printFullName2 person = printfn "Name is %s" (person.FullName) 

Dieser Code wird überhaupt nicht kompiliert, weil Die Typinferenz verfügt nicht über genügend Informationen, um den Parametertyp zu bestimmen. Jedes Objekt kann .FullName implementieren - dies reicht für die Ausgabe nicht aus.


Ja, wir können eine Funktion mit einem Parametertyp versehen, aber dadurch geht der gesamte Punkt der automatischen Typinferenz verloren.


Methoden passen schlecht zu Funktionen höherer Ordnung


Ein ähnliches Problem tritt bei Funktionen höherer Ordnung auf. Zum Beispiel gibt es eine Liste von Personen, und wir müssen eine Liste ihrer vollständigen Namen erhalten.


Bei einer unabhängigen Funktion ist die Lösung trivial:


 open Person let list = [ Person.create "Andy" "Anderson"; Person.create "John" "Johnson"; Person.create "Jack" "Jackson"] //     list |> List.map fullName 

Bei einer Objektmethode müssen Sie überall ein spezielles Lambda erstellen:


 open Person let list = [ Person.create "Andy" "Anderson"; Person.create "John" "Johnson"; Person.create "Jack" "Jackson"] //    list |> List.map (fun p -> p.FullName) 

Dies ist jedoch immer noch ein ziemlich einfaches Beispiel. Methoden von Objekten sind für die Komposition durchaus zugänglich, in der Pipeline unpraktisch usw.


Wenn Sie mit funktionaler Programmierung noch nicht vertraut sind, fordere ich Sie daher dringend auf: Wenn Sie können, verwenden Sie keine Methoden, insbesondere nicht im Lernprozess. Es handelt sich um eine Krücke, mit der Sie nicht den größtmöglichen Nutzen aus der funktionalen Programmierung ziehen können.


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


All Articles