Funktionales Denken. Teil 9

Dies ist bereits Teil 9 einer Artikelserie zur funktionalen Programmierung in F #! Ich bin sicher, dass es auf Habré nicht sehr viele so lange Zyklen gibt. Aber wir werden nicht aufhören. Heute werden wir über verschachtelte Funktionen, Module, Namespaces und das Mischen von Typen und Funktionen in Modulen sprechen.






Jetzt wissen Sie, wie man Funktionen definiert, aber wie man sie organisiert?


F # hat drei Möglichkeiten:


  • Funktionen können in andere Funktionen verschachtelt werden.
  • Auf Anwendungsebene werden Funktionen der obersten Ebene in „Module“ gruppiert.
  • oder Sie können einen objektorientierten Ansatz verfolgen und Funktionen als Methoden an Typen anhängen.

In diesem Artikel werden wir die ersten beiden Methoden und die verbleibende in der nächsten betrachten.


Verschachtelte Funktionen


In F # können Sie Funktionen innerhalb anderer Funktionen definieren. Dies ist eine gute Möglichkeit, Hilfsfunktionen zu kapseln, die nur für die Hauptfunktion benötigt werden und von außen nicht sichtbar sein sollten.


Im folgenden Beispiel ist add in addThreeNumbers verschachtelt:


 let addThreeNumbers xyz = //     let add n = fun x -> x + n //    x |> add y |> add z addThreeNumbers 2 3 4 

Verschachtelte Funktionen können direkt auf die übergeordneten Parameter zugreifen, da sie sich in ihrem Bereich befinden.
Im folgenden Beispiel printError verschachtelte Funktion printError keine Parameter, da sie kann direkt auf n und max zugreifen.


 let validateSize max n = //       let printError() = printfn "Oops: '%i' is bigger than max: '%i'" n max //    if n > max then printError() validateSize 10 9 validateSize 10 11 

Ein sehr häufiges Muster ist die Hauptfunktion, die die verschachtelte rekursive Hilfsfunktion definiert, die mit den entsprechenden Anfangswerten aufgerufen wird.
Das Folgende ist ein Beispiel für einen solchen Code:


 let sumNumbersUpTo max = //      let rec recursiveSum n sumSoFar = match n with | 0 -> sumSoFar | _ -> recursiveSum (n-1) (n+sumSoFar) //       recursiveSum max 0 sumNumbersUpTo 10 

Vermeiden Sie eine tiefe Verschachtelung, insbesondere bei direktem Zugriff (nicht in Form von Parametern) auf übergeordnete Variablen.
Übermäßig tief verschachtelte Funktionen sind ebenso schwer zu verstehen wie die schlimmsten von vielen verschachtelten imperativen Zweigen.


Ein Beispiel dafür, wie man es nicht macht:


 // wtf,    ? let fx = let f2 y = let f3 z = x * z let f4 z = let f5 z = y * z let f6 () = y * x f6() f4 y x * f2 x 

Module


Ein Modul ist einfach eine Sammlung von Funktionen, die zusammen gruppiert sind, normalerweise weil sie mit demselben Datentyp oder denselben Datentypen arbeiten.


Eine Moduldefinition ist einer Funktionsdefinition sehr ähnlich. Es beginnt mit dem Schlüsselwort module , dann kommt das Zeichen = , gefolgt vom Inhalt des Moduls.
Der Inhalt des Moduls muss mit einem Offset formatiert sein, ebenso wie die Ausdrücke in der Definition von Funktionen.


Definition eines Moduls mit zwei Funktionen:


 module MathStuff = let add xy = x + y let subtract xy = x - y 

Wenn Sie diesen Code in Visual Studio öffnen add können Sie beim MathStuff.add des MathStuff.add über add den vollständigen Namen add , der tatsächlich MathStuff.add , als wäre MastStuff eine Klasse und add eine Methode.


Genau das passiert gerade. Hinter den Kulissen erstellt der F # -Compiler eine statische Klasse mit statischen Methoden. Das C # -Äquivalent würde folgendermaßen aussehen:


 static class MathStuff { static public int add(int x, int y) { return x + y; } static public int subtract(int x, int y) { return x - y; } } 

Wenn Sie erkennen, dass Module nur statische Klassen und Funktionen statische Methoden sind, erhalten Sie ein gutes Verständnis der Funktionsweise von Modulen in F #, da die meisten Regeln, die für statische Klassen gelten, auch für Module gelten.


Und genau wie in C # sollte jede eigenständige Funktion Teil der Klasse sein, in F # sollte jede eigenständige Funktion Teil des Moduls sein.


Zugriff auf Funktionen außerhalb des Moduls


Wenn Sie von einem anderen Modul aus auf eine Funktion zugreifen müssen, können Sie über den vollständigen Namen darauf verweisen.


 module MathStuff = let add xy = x + y let subtract xy = x - y module OtherStuff = //     MathStuff let add1 x = MathStuff.add x 1 

Sie können auch alle Funktionen eines anderen Moduls mithilfe der open Direktive importieren. Anschließend können Sie den Kurznamen anstelle des vollständigen Namens verwenden.


 module OtherStuff = open MathStuff //      let add1 x = add x 1 

Die Regeln für die Verwendung von Namen werden durchaus erwartet. Sie können jederzeit über den vollständigen Namen auf eine Funktion zugreifen oder je nach aktuellem Bereich relative oder unvollständige Namen verwenden.


Verschachtelte Module


Module können wie statische Klassen verschachtelte Module enthalten:


 module MathStuff = let add xy = x + y let subtract xy = x - y //   module FloatLib = let add xy :float = x + y let subtract xy :float = x - y 

Andere Module können auf Funktionen in verschachtelten Modulen verweisen, wobei der vollständige oder relative Name verwendet wird:


 module OtherStuff = open MathStuff let add1 x = add x 1 //   let add1Float x = MathStuff.FloatLib.add x 1.0 //   let sub1Float x = FloatLib.subtract x 1.0 

Module der obersten Ebene


Da die Module verschachtelt werden können, können Sie in der Kette ein übergeordnetes Modul der obersten Ebene erreichen. Das ist tatsächlich so.


Module der obersten Ebene werden anders definiert als die zuvor gezeigten Module.


  • Die module MyModuleName muss die erste Deklaration in der Datei sein
  • Zeichen = fehlt
  • Der Modulinhalt darf nicht eingerückt werden

Im Allgemeinen sollte in jeder .FS Quelldatei .FS Deklaration der obersten Ebene vorhanden sein. Es gibt einige Ausnahmen, aber es ist immer noch eine gute Praxis. Der Modulname muss nicht mit dem Dateinamen übereinstimmen, aber zwei Dateien dürfen keine Module mit demselben Namen enthalten.


Für .FSX Dateien wird die Moduldeklaration nicht benötigt. In diesem Fall wird der Name der Skriptdatei automatisch zum .FSX .


Ein Beispiel für ein MathStuff , das als "Top-Modul" -Modul deklariert wurde:


 //    module MathStuff let add xy = x + y let subtract xy = x - y //   module FloatLib = let add xy :float = x + y let subtract xy :float = x - y 

Beachten Sie, dass der Code der "obersten Ebene" ( module MathStuff ist) keinen Einzug module MathStuff , während der Inhalt eines verschachtelten FloatLib Moduls noch eingerückt werden muss.


Sonstige Modulinhalte


Neben Funktionen können Module auch andere Deklarationen enthalten, z. B. Typdeklarationen, einfache Werte und Initialisierungscode (z. B. statische Konstruktoren).


 module MathStuff = //  let add xy = x + y let subtract xy = x - y //   type Complex = {r:float; i:float} type IntegerFunction = int -> int -> int type DegreesOrRadians = Deg | Rad // "" let PI = 3.141 // "" let mutable TrigType = Deg //  /   do printfn "module initialized" 

Wenn Sie diese Beispiele interaktiv ausführen, müssen Sie die Sitzung möglicherweise häufig genug neu starten, damit der Code „frisch“ bleibt und nicht durch vorherige Berechnungen infiziert wird.


Verschleierung (Überlappung, Beschattung)


Dies ist wieder unser Beispielmodul. Beachten Sie, dass MathStuff die add Funktion sowie MathStuff enthält.


 module MathStuff = let add xy = x + y let subtract xy = x - y //   module FloatLib = let add xy :float = x + y let subtract xy :float = x - y 

Was passiert, wenn Sie beide Module im aktuellen Bereich öffnen und add aufrufen?


 open MathStuff open MathStuff.FloatLib let result = add 1 2 // Compiler error: This expression was expected to // have type float but here has type int 

Und es kam vor, dass das MathStuff.FloatLib Modul das ursprüngliche MathStuff neu MathStuff , das vom FloatLib Modul blockiert (ausgeblendet) wurde.


Als Ergebnis erhalten wir den FS0001-Compilerfehler, da der erste Parameter 1 als Float erwartet wurde. Um dies zu beheben, müssen Sie 1 in 1.0 ändern.


Leider wird dies in der Praxis diskret und leicht übersehen. Manchmal können Sie mit dieser Technik interessante Tricks ausführen, fast wie Unterklassen, aber meistens ist das Vorhandensein gleichnamiger Funktionen ärgerlich (zum Beispiel bei der extrem häufigen map ).


Wenn Sie dieses Verhalten vermeiden möchten, können Sie es mit dem RequireQualifiedAccess Attribut RequireQualifiedAccess . Das gleiche Beispiel, in dem beide Module mit diesem Attribut dekoriert sind:


 [<RequireQualifiedAccess>] module MathStuff = let add xy = x + y let subtract xy = x - y //   [<RequireQualifiedAccess>] module FloatLib = let add xy :float = x + y let subtract xy :float = x - y 

Jetzt ist die open Richtlinie nicht verfügbar:


 open MathStuff //  open MathStuff.FloatLib //  

Sie können jedoch weiterhin (ohne Mehrdeutigkeit) über ihre vollständigen Namen auf Funktionen zugreifen:


 let result = MathStuff.add 1 2 let result = MathStuff.FloatLib.add 1.0 2.0 

Zugangskontrolle


F # unterstützt die Verwendung von Standard-.NET-Zugriffssteuerungsoperatoren wie public , private und internal . Der MSDN-Artikel enthält vollständige Informationen.


  • Diese Zugriffsspezifizierer können auf Funktionen, Werte, Typen und andere Deklarationen der obersten Ebene in einem Modul angewendet werden ("gebunden lassen"). Sie können auch für die Module selbst angegeben werden (beispielsweise wird möglicherweise ein privates verschachteltes Modul benötigt).
  • Standardmäßig hat alles öffentlichen Zugriff (mit Ausnahme einiger Fälle). Um sie zu schützen, müssen Sie private oder internal .

Diese Zugriffsspezifizierer sind nur eine Möglichkeit, die Sichtbarkeit in F # zu steuern. Eine völlig andere Möglichkeit besteht darin, Signaturdateien zu verwenden, die C-Header-Dateien ähneln. Sie beschreiben abstrakt den Inhalt des Moduls. Signaturen sind sehr nützlich für eine ernsthafte Kapselung. Um jedoch ihre Funktionen zu berücksichtigen, müssen Sie auf die geplante Serie zu Kapselung und Sicherheit warten, die auf Funktionen basiert .


Namespaces


Namespaces in F # ähneln Namespaces in C #. Sie können verwendet werden, um Module und Typen zu organisieren, um Namenskonflikte zu vermeiden.


Ein mit dem namespace Schlüsselwort deklarierter namespace :


 namespace Utilities module MathStuff = //  let add xy = x + y let subtract xy = x - y 

Aufgrund dieses Namespace wurde der vollständige Name des MathStuff Moduls zu Utilities.MathStuff , und der vollständige Name add lautet Utilities.MathStuff.add .


Die gleichen Einrückungsregeln gelten für Module in einem Namespace, die oben für Module gezeigt wurden.


Sie können einen Namespace auch explizit deklarieren, indem Sie dem Modulnamen einen Punkt hinzufügen. Das heißt, Der obige Code kann folgendermaßen umgeschrieben werden:


 module Utilities.MathStuff //  let add xy = x + y let subtract xy = x - y 

Der vollständige Name des MathStuff Moduls lautet weiterhin Utilities.MathStuff . Jetzt handelt es sich jedoch um ein Modul der obersten Ebene, dessen Inhalt nicht MathStuff werden muss.


Einige zusätzliche Funktionen zur Verwendung von Namespaces:


  • Namespaces sind für Module optional. Im Gegensatz zu C # gibt es für F # -Projekte keinen Standard-Namespace, sodass ein Modul der obersten Ebene ohne Namespace global ist. Wenn Sie wiederverwendbare Bibliotheken erstellen möchten, müssen Sie mehrere Namespaces hinzufügen, um Konflikte mit dem Code anderer Bibliotheken zu vermeiden.
  • Namespaces können direkt Typdeklarationen enthalten, jedoch keine Funktionsdeklarationen. Wie bereits erwähnt, müssen alle Funktions- und Wertdeklarationen Teil eines Moduls sein.
  • Beachten Sie schließlich, dass Namespaces in Skripten nicht funktionieren. Wenn Sie beispielsweise versuchen, eine Namespace-Deklaration, z. B. namespace Utilities , an ein interaktives Fenster zu senden, wird ein Fehler empfangen.

Namespace-Hierarchie


Sie können eine Hierarchie von Namespaces erstellen, indem Sie die Namen einfach durch Punkte teilen:


 namespace Core.Utilities module MathStuff = let add xy = x + y 

Sie können auch zwei Namespaces in einer Datei deklarieren, wenn Sie möchten. Es ist zu beachten, dass alle Namespaces mit ihrem vollständigen Namen deklariert werden müssen - sie unterstützen keine Verschachtelung.


 namespace Core.Utilities module MathStuff = let add xy = x + y namespace Core.Extra module MoreMathStuff = let add xy = x + y 

Ein Namenskonflikt zwischen dem Namespace und dem Modul ist nicht möglich.


 namespace Core.Utilities module MathStuff = let add xy = x + y namespace Core //    - Core.Utilities //     ! module Utilities = let add xy = x + y 

Mischen von Typen und Funktionen in Modulen


Wie wir gesehen haben, bestehen Module normalerweise aus vielen voneinander abhängigen Funktionen, die mit einem bestimmten Datentyp interagieren.


In OOP würden darüber liegende Datenstrukturen und Funktionen innerhalb einer Klasse kombiniert. In der funktionalen F # werden Datenstrukturen und Funktionen darüber zu einem Modul zusammengefasst.


Es gibt zwei Muster zum Kombinieren von Typen und Funktionen:


  • Typ wird getrennt von Funktionen deklariert
  • Typ wird im selben Modul wie Funktionen deklariert

Im ersten Fall wird der Typ außerhalb eines Moduls (aber im Namespace) deklariert. Danach werden die Funktionen, die mit diesem Typ arbeiten, in das Modul desselben Typs eingefügt.


 //    namespace Example //      type PersonType = {First:string; Last:string} //    ,     module Person = //  let create first last = {First=first; Last=last} // ,     let fullName {First=first; Last=last} = first + " " + last let person = Person.create "john" "doe" Person.fullName person |> printfn "Fullname=%s" 

Alternativ wird der Typ innerhalb des Moduls deklariert und hat einen einfachen Namen wie " T " oder den Namen des Moduls. Der Zugriff auf Funktionen ist ungefähr wie folgt: MyModule.Func und MyModule.Func2 und Zugriff auf den Typ: MyModule.T :


 module Customer = // Customer.T -      type T = {AccountId:int; Name:string} //  let create id name = {T.AccountId=id; T.Name=name} // ,     let isValid {T.AccountId=id; } = id > 0 let customer = Customer.create 42 "bob" Customer.isValid customer |> printfn "Is valid?=%b" 

Beachten Sie, dass in beiden Fällen eine Konstruktorfunktion vorhanden sein muss, die eine neue Instanz des Typs (Factory) erstellt. Dann müssen Sie im Client-Code kaum explizit auf den Typnamen zugreifen und müssen sich nicht fragen, ob sich der Typ im Modul befindet oder nicht.


Welchen Weg wählen?


  • Der erste Ansatz ähnelt eher klassischem .NET und sollte bevorzugt werden, wenn Sie diese Bibliothek für Code außerhalb von F # verwenden möchten, für den eine separat vorhandene Klasse erwartet wird.
  • Der zweite Ansatz ist in anderen funktionalen Sprachen üblicher. Der Typ innerhalb des Moduls wird als verschachtelte Klasse kompiliert, was für OOP-Sprachen normalerweise nicht sehr praktisch ist.

Sie können selbst mit beiden Methoden experimentieren. Bei der Teamentwicklung muss ein Stil gewählt werden.


Module, die nur Typen enthalten


Wenn es viele Typen gibt, die ohne Funktionen deklariert werden müssen, verwenden Sie das Modul nicht. Sie können Typen direkt im Namespace deklarieren, ohne auf verschachtelte Klassen zurückgreifen zu müssen.


Zum Beispiel möchten Sie vielleicht Folgendes tun:


 //    module Example //     type PersonType = {First:string; Last:string} //    ,  ... 

Und hier ist eine andere Möglichkeit, dasselbe zu tun. Das module einfach durch den namespace .


 //    namespace Example //     type PersonType = {First:string; Last:string} 

In beiden Fällen hat PersonType denselben vollständigen Namen.


Bitte beachten Sie, dass dieser Ersatz nur mit Typen funktioniert. Funktionen müssen immer innerhalb des Moduls deklariert werden.


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


All Articles