Der dritte Teil einer Reihe von Artikeln zur funktionalen Programmierung ist erschienen. Heute werden wir über alle Arten dieses Paradigmas sprechen und Beispiele für ihre Verwendung zeigen. Weitere Informationen zu primitiven Typen, verallgemeinerten Typen und vielem mehr unter dem Schnitt!

Nachdem wir die Funktionen verstanden haben, werden wir sehen, wie Typen mit Funktionen wie Domäne und Bereich interagieren. Dieser Artikel ist nur eine Überprüfung. Für ein tieferes Eintauchen in Typen gibt es eine Reihe von "F # -Typen verstehen" .
Zu Beginn benötigen wir ein etwas besseres Verständnis der Typennotation. Wir haben die Pfeilnotation " ->
" gesehen, die Domäne und Bereich trennt. Die Funktionssignatur sieht also immer so aus:
val functionName : domain -> range
Einige weitere Beispiele für Funktionen:
let intToString x = sprintf "x is %i" x // int string let stringToInt x = System.Int32.Parse(x)
Wenn Sie diesen Code in einem interaktiven Fenster ausführen, werden die folgenden Signaturen angezeigt:
val intToString : int -> string val stringToInt : string -> int
Sie bedeuten:
intToString
hat eine Domäne vom Typ int
, die dem Bereich der intToString
ist.stringToInt
verfügt über eine Domäne vom Typ string
, die einem Bereich vom Typ int
.
Primitive Typen
Es werden erwartete primitive Typen erwartet: Zeichenfolge, int, float, bool, char, byte usw. sowie viele andere Ableitungen des .NET-Typsystems.
Einige weitere Beispiele für Funktionen mit primitiven Typen:
let intToFloat x = float x // "float" - int float let intToBool x = (x = 2) // true x 2 let stringToString x = x + " world"
und ihre Unterschriften:
val intToFloat : int -> float val intToBool : int -> bool val stringToString : string -> string
Geben Sie Annotation ein
In den vorherigen Beispielen hat der F # -Compiler die Arten von Parametern und Ergebnissen korrekt definiert. Dies ist jedoch nicht immer der Fall. Wenn Sie versuchen, den folgenden Code auszuführen, wird ein Kompilierungsfehler angezeigt:
let stringLength x = x.Length => error FS0072: Lookup on object of indeterminate type
Der Compiler kennt den Typ des Arguments "x" nicht und weiß daher nicht, ob die "Länge" eine gültige Methode ist. In den meisten Fällen kann dies behoben werden, indem die "Typanmerkung" an den F # -Compiler übergeben wird. Dann wird er wissen, welchen Typ er verwenden soll. In der festen Version geben wir an, dass der Typ "x" eine Zeichenfolge ist.
let stringLength (x:string) = x.Length
Die geschweiften Klammern um den Parameter x:string
sind wichtig. Wenn sie übersprungen werden, entscheidet der Compiler, dass die Zeichenfolge der Rückgabewert ist! Das heißt, ein offener Doppelpunkt wird verwendet, um den Typ des Rückgabewerts anzugeben, wie im folgenden Beispiel gezeigt.
let stringLengthAsInt (x:string) :int = x.Length
Wir geben an, dass der Parameter x
eine Zeichenfolge und der Rückgabewert eine Ganzzahl ist.
Funktionstypen als Parameter
Eine Funktion, die andere Funktionen als Parameter verwendet oder eine Funktion zurückgibt, wird als Funktion höherer Ordnung bezeichnet ( Funktion höherer Ordnung wird manchmal auf HOF verkürzt). Sie werden als Abstraktion verwendet, um ein möglichst allgemeines Verhalten festzulegen. Diese Art von Funktion ist in F # sehr verbreitet, die meisten Standardbibliotheken verwenden sie.
Betrachten Sie die Funktion evalWith5ThenAdd2
, die eine Funktion als Parameter verwendet, diese Funktion dann aus 5 berechnet und dem Ergebnis 2 hinzufügt:
let evalWith5ThenAdd2 fn = fn 5 + 2 // , fn(5) + 2
Die Signatur dieser Funktion sieht folgendermaßen aus:
val evalWith5ThenAdd2 : (int -> int) -> int
Sie können sehen, dass die Domäne (int->int)
und der Bereich int
. Was bedeutet das? Dies bedeutet, dass der Eingabeparameter kein einfacher Wert ist, sondern eine Funktion aus vielen Funktionen von int
bis int
. Der Ausgabewert ist keine Funktion, sondern nur ein int
.
Versuchen wir mal:
let add1 x = x + 1 // - (int -> int) evalWith5ThenAdd2 add1 //
und bekommen:
val add1 : int -> int val it : int = 8
" add1
" ist eine Funktion, die int
auf int
abbildet, wie wir aus der Signatur sehen. Es ist ein gültiger Parameter für evalWith5ThenAdd2
und das Ergebnis ist 8.
Das spezielle Wort " it
" bezeichnet übrigens den zuletzt berechneten Wert. In diesem Fall ist es das Ergebnis, auf das wir gewartet haben. Dies ist kein Schlüsselwort, sondern nur eine Namenskonvention.
Ein anderer Fall:
let times3 x = x * 3 // - (int -> int) evalWith5ThenAdd2 times3 //
gibt:
val times3 : int -> int val it : int = 17
" times3
" ist auch eine Funktion, die int
auf int
abbildet, wie aus der Signatur ersichtlich ist. Es ist auch ein gültiger Parameter für evalWith5ThenAdd2
. Das Ergebnis der Berechnungen ist 17.
Bitte beachten Sie, dass die Eingabedaten typabhängig sind. Wenn die übergebene Funktion ein float
und kein int
, funktioniert nichts. Zum Beispiel, wenn wir haben:
let times3float x = x * 3.0 // - (float->float) evalWith5ThenAdd2 times3float
Der Compiler gibt beim Kompilieren einen Fehler zurück:
error FS0001: Type mismatch. Expecting a int -> int but given a float -> float
Meldung, dass die Eingabefunktion eine Funktion vom Typ int->int
.
Funktioniert als Ausgabe
Wertfunktionen können auch das Ergebnis von Funktionen sein. Die folgende Funktion generiert beispielsweise eine "Addierer" -Funktion, die einen Eingabewert hinzufügt.
let adderGenerator numberToAdd = (+) numberToAdd
Ihre Unterschrift:
val adderGenerator : int -> (int -> int)
bedeutet, dass der Generator ein int
und eine Funktion ("Addierer") erstellt, die ints
ints
. Mal sehen, wie es funktioniert:
let add1 = adderGenerator 1 let add2 = adderGenerator 2
Es werden zwei Addiererfunktionen erstellt. Die erste erstellt eine Funktion, die der Eingabe 1 hinzufügt, die zweite fügt 2 hinzu. Beachten Sie, dass die Signaturen genau den Erwartungen entsprechen.
val add1 : (int -> int) val add2 : (int -> int)
Jetzt können Sie die generierten Funktionen wie gewohnt verwenden, sie unterscheiden sich nicht von den explizit definierten Funktionen:
add1 5 // val it : int = 6 add2 5 // val it : int = 7
Verwenden von Typanmerkungen zum Einschränken von Funktionstypen
Im ersten Beispiel haben wir uns eine Funktion angesehen:
let evalWith5ThenAdd2 fn = fn 5 +2 > val evalWith5ThenAdd2 : (int -> int) -> int
In diesem Beispiel kann F # schließen, dass " fn
" int
in int
konvertiert, sodass seine Signatur int->int
.
Aber wie lautet die Signatur von "fn" im folgenden Fall?
let evalWith5 fn = fn 5
Es ist klar, dass " fn
" eine Art Funktion ist, die ein int
, aber was gibt es zurück? Der Compiler kann diese Frage nicht beantworten. In solchen Fällen können Sie, wenn der Funktionstyp angegeben werden muss, einen Anmerkungstyp für Funktionsparameter sowie für primitive Typen hinzufügen.
let evalWith5AsInt (fn:int->int) = fn 5 let evalWith5AsFloat (fn:int->float) = fn 5
Darüber hinaus können Sie den Rückgabetyp bestimmen.
let evalWith5AsString fn :string = fn 5
Weil Die Hauptfunktion gibt einen string
, die Funktion " fn
" muss ebenfalls einen string
. Daher ist es nicht erforderlich, den Typ " fn
" explizit anzugeben.
Geben Sie "Einheit" ein
Während des Programmierprozesses möchten wir manchmal, dass eine Funktion etwas tut, ohne etwas zurückzugeben. Betrachten Sie die Funktion " printInt
". Die Funktion gibt wirklich nichts zurück. Es druckt einfach die Zeichenfolge als Nebeneffekt der Ausführung auf die Konsole.
let printInt x = printf "x is %i" x //
Was ist ihre Unterschrift?
val printInt : int -> unit
Was ist eine " unit
"?
Auch wenn die Funktion keine Werte zurückgibt, benötigt sie dennoch einen Bereich. In der Welt der Mathematik gibt es keine "leeren" Funktionen. Jede Funktion muss etwas zurückgeben, da die Funktion ein Mapping ist und das Mapping etwas anzeigen muss!

In F # geben Funktionen wie diese einen speziellen Ergebnistyp namens " unit
" zurück. Es enthält nur einen Wert, der mit " ()
" gekennzeichnet ist. Sie könnten denken, dass unit
und ()
so etwas wie "void" bzw. "null" von C # sind. Aber im Gegensatz zu ihnen ist unit
der reale Typ und ()
reale Wert. Um dies zu überprüfen, gehen Sie einfach wie folgt vor:
let whatIsThis = ()
Die folgende Unterschrift wird erhalten:
val whatIsThis : unit = ()
whatIsThis
zeigt an, dass die Bezeichnung " whatIsThis
" vom Typ unit
und einem Wert ()
.
printInt
wir nun zur " printInt
" printInt
zurückkehren, können wir die Bedeutung dieses Eintrags verstehen:
val printInt : int -> unit
Diese Signatur besagt, dass printInt
eine Domäne von int
, was sich in etwas übersetzt, das uns nicht interessiert.
Funktionen ohne Parameter
Können wir, unit
wir die unit
, ihr Auftreten in einem anderen Kontext vorhersagen? Versuchen Sie beispielsweise, eine wiederverwendbare Funktion "Hallo Welt" zu erstellen. Da es keine Eingabe oder Ausgabe gibt, können wir die Signatureinheit unit -> unit
erwarten. Mal sehen:
let printHello = printf "hello world" //
Ergebnis:
hello world val printHello : unit = ()
Nicht ganz das, was wir erwartet hatten. "Hallo Welt" wurde sofort ausgegeben und das Ergebnis war keine Funktion, sondern ein einfacher Wert vom Typ Einheit. Wir können sagen, dass dies ein einfacher Wert ist, da er, wie wir zuvor gesehen haben, eine Signatur des Formulars hat:
val aName: type = constant
In diesem Beispiel sehen wir, dass printHello
wirklich ein einfacher Wert ist ()
. Dies ist keine Funktion, die wir später aufrufen können.
Was ist der Unterschied zwischen printInt
und printHello
? Im Fall von printInt
Wert erst bestimmt werden, wenn wir den Wert des Parameters x
. Die Definition war also eine Funktion. Im Fall von printHello
gibt es keine Parameter, so dass die rechte Seite an Ort und Stelle definiert werden kann. Und es war gleich ()
mit einem Nebeneffekt in Form der Ausgabe an die Konsole.
Sie können eine echte wiederverwendbare Funktion ohne Parameter erstellen und die Definition dazu zwingen, ein unit
zu haben:
let printHelloFn () = printf "hello world" //
Jetzt ist ihre Unterschrift gleich:
val printHelloFn : unit -> unit
und um es aufzurufen, müssen wir ()
als Parameter übergeben:
printHelloFn ()
Verstärkung von Einheitentypen mit der Ignorierfunktion
In einigen Fällen benötigt der Compiler einen unit
und beschwert sich. Beispielsweise verursachen beide der folgenden Fälle einen Compilerfehler:
do 1+1 // => FS0020: This expression should have type 'unit' let something = 2+2 // => FS0020: This expression should have type 'unit' "hello"
Um in diesen Situationen zu helfen, gibt es eine spezielle ignore
, die alles übernimmt und die unit
zurückgibt. Die richtige Version dieses Codes könnte folgende sein:
do (1+1 |> ignore) // ok let something = 2+2 |> ignore // ok "hello"
Generische Typen
In den meisten Fällen müssen wir etwas dazu sagen, wenn der Typ eines Funktionsparameters ein beliebiger Typ sein kann. F # verwendet für solche Situationen .NET-Generika.
Die folgende Funktion konvertiert beispielsweise einen Parameter durch Hinzufügen von Text in eine Zeichenfolge:
let onAStick x = x.ToString() + " on a stick"
ToString()
von der Art des Parameters können alle Objekte in ToString()
.
Unterschrift:
val onAStick : 'a -> string
Welcher Typ 'a
? In F # ist dies eine Möglichkeit, einen generischen Typ anzugeben, der zur Kompilierungszeit unbekannt ist. Ein Apostroph vor "a" bedeutet, dass der Typ generisch ist. Entspricht dieser Signatur in C #:
string onAStick<a>(); // string OnAStick<TObject>(); // F#- 'a // C#'- "TObject"
Es versteht sich, dass diese F # -Funktion auch bei generischen Typen eine starke Typisierung aufweist. Es wird kein Parameter vom Typ Object
akzeptiert. Starkes Tippen ist gut, da Sie damit die Typensicherheit beim Erstellen von Funktionen beibehalten können.
Die gleiche Funktion wird für int
, float
und string
.
onAStick 22 onAStick 3.14159 onAStick "hello"
Wenn es zwei verallgemeinerte Parameter gibt, gibt der Compiler ihnen zwei verschiedene Namen: 'a
für den ersten 'b
für den zweiten usw. Zum Beispiel:
let concatString xy = x.ToString() + y.ToString()
Diese Signatur enthält zwei generische Typen: 'a
und 'b
:
val concatString : 'a -> 'b -> string
Andererseits erkennt der Compiler, wenn nur ein generischer Typ erforderlich ist. Im folgenden Beispiel müssen x
und y
vom gleichen Typ sein:
let isEqual xy = (x=y)
Eine Funktionssignatur hat also für beide Parameter denselben generischen Typ:
val isEqual : 'a -> 'a -> bool
Verallgemeinerte Parameter sind auch sehr wichtig, wenn es um Listen und andere abstrakte Strukturen geht, und wir werden viele davon in den folgenden Beispielen sehen.
Andere Arten
Bisher wurden nur Grundtypen diskutiert. Diese Typen können auf verschiedene Weise zu komplexeren Typen kombiniert werden. Ihre vollständige Analyse wird später in einer anderen Reihe erfolgen , aber in der Zwischenzeit werden wir sie hier kurz analysieren, damit Sie sie in den Signaturen von Funktionen erkennen können.
- Tupel Dies ist ein Paar, ein Tripel usw., das aus anderen Typen besteht. Zum Beispiel ist
("hello", 1)
ein Tupel, das auf string
und int
basiert. Ein Komma ist ein Kennzeichen von Tupeln. Wenn irgendwo in F # ein Komma angezeigt wird, ist dies fast garantiert Teil des Tupels.
In Funktionssignaturen werden Tupel als „Produkte“ der beiden beteiligten Typen geschrieben. In diesem Fall ist das Tupel vom Typ:
string * int // ("hello", 1)
- Sammlungen . Die häufigsten sind list (list), seq (sequence) und array. Listen und Arrays haben eine feste Größe, während Sequenzen möglicherweise unendlich sind (hinter den Kulissen sind Sequenzen dieselbe
IEnumrable
). In Funktionssignaturen haben sie ihre eigenen Schlüsselwörter: " list
", " seq
" und " []
" für Arrays.
int list // List type [1;2;3] string list // List type ["a";"b";"c"] seq<int> // Seq type seq{1..10} int [] // Array type [|1;2;3|]
- Option (optionaler Typ) . Dies ist ein einfacher Wrapper über Objekte, die möglicherweise fehlen. Es gibt zwei Optionen:
Some
(wenn der Wert vorhanden ist) und None
(wenn der Wert nicht vorhanden ist). In Funktionssignaturen haben sie ein eigenes Schlüsselwort " option
":
int option // Some 1
- Die markierte Assoziation (diskriminierte Gewerkschaft) . Sie bestehen aus vielen Variationen anderer Typen. Wir haben einige Beispiele in "Warum F # verwenden?" . In Funktionssignaturen werden sie nach Typnamen referenziert, sie haben kein spezielles Schlüsselwort.
- Datensatztyp (Datensätze) . Typen wie Datenbankstrukturen oder Zeilen, eine Reihe benannter Werte. Wir haben auch einige Beispiele in "Warum F # verwenden?" . In Funktionssignaturen werden sie nach Typnamen aufgerufen und haben auch kein eigenes Schlüsselwort.
Testen Sie Ihr Verständnis von Typen
Hier sind einige Ausdrücke, um Ihr Verständnis von Funktionssignaturen zu testen. Um dies zu überprüfen, führen Sie sie einfach in einem interaktiven Fenster aus!
let testA = float 2 let testB x = float 2 let testC x = float 2 + x let testD x = x.ToString().Length let testE (x:float) = x.ToString().Length let testF x = printfn "%s" x let testG x = printfn "%f" x let testH = 2 * 2 |> ignore let testI x = 2 * 2 |> ignore let testJ (x:int) = 2 * 2 |> ignore let testK = "hello" let testL() = "hello" let testM x = x=x let testN x = x 1 // : x? let testO x:string = x 1 // : :string ?
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.