Tour of F#

Die beste Möglichkeit, F# zu erlernen, besteht im Lesen und Schreiben von F#-Code. Dieser Artikel bietet eine Tour durch einige der wichtigsten Features von F# und bietet Ihnen einige Codeausschnitte, die Sie auf Ihrem Computer ausführen können. Informationen zum Einrichten einer Entwicklungsumgebung finden Sie unter Erste Schritte.

Es gibt zwei hauptkonzepte in F#: Funktionen und Typen. In dieser Tour werden die Features der Sprache hervorgehoben, die in diese beiden Konzepte fallen.

Onlineausführung des Codes

Wenn F# nicht auf Ihrem Computer installiert ist, können Sie alle Beispiele in Ihrem Browser mit F# ausprobieren in Fable ausführen. Fable ist ein Dialekt von F#, der direkt in Ihrem Browser ausgeführt wird. Die folgenden Beispiele finden Sie unter Beispiele > Learn > Tour of F# auf der linken Menüleiste der Fable REPL.

Funktionen und Module

Die grundlegendsten Teile eines F#-Programms sind Funktionen, die in _-Modulen organisiert sind**. Funktionen arbeiten an Eingaben, um Ausgaben zu erzeugen, und sind unter Moduleorganisiert. Dies ist die primäre Methode, um Dies in F# zu gruppen. Sie werden mithilfe der Bindung let definiert,die der Funktion einen Namen gibt und ihre Argumente definiert.

module BasicFunctions = 

    /// You use 'let' to define a function. This one accepts an integer argument and returns an integer. 
    /// Parentheses are optional for function arguments, except for when you use an explicit type annotation.
    let sampleFunction1 x = x*x + 3

    /// Apply the function, naming the function return result using 'let'. 
    /// The variable type is inferred from the function return type.
    let result1 = sampleFunction1 4573

    // This line uses '%d' to print the result as an integer. This is type-safe.
    // If 'result1' were not of type 'int', then the line would fail to compile.
    printfn $"The result of squaring the integer 4573 and adding 3 is %d{result1}"

    /// When needed, annotate the type of a parameter name using '(argument:type)'.  Parentheses are required.
    let sampleFunction2 (x:int) = 2*x*x - x/5 + 3

    let result2 = sampleFunction2 (7 + 4)
    printfn $"The result of applying the 2nd sample function to (7 + 4) is %d{result2}"

    /// Conditionals use if/then/elif/else.
    ///
    /// Note that F# uses white space indentation-aware syntax, similar to languages like Python.
    let sampleFunction3 x = 
        if x < 100.0 then 
            2.0*x*x - x/5.0 + 3.0
        else 
            2.0*x*x + x/5.0 - 37.0

    let result3 = sampleFunction3 (6.5 + 4.5)

    // This line uses '%f' to print the result as a float.  As with '%d' above, this is type-safe.
    printfn $"The result of applying the 3rd sample function to (6.5 + 4.5) is %f{result3}"

let Bindungen sind auch die Art und Weise, wie Sie einen Wert an einen Namen binden, ähnlich wie bei einer Variablen in anderen Sprachen. letBindungen sindstandardmäßig * unveränderlich _ . Das bedeutet, dass ein Wert oder eine Funktion, sobald er an einen Namen gebunden ist, nicht mehr an Ort und Stelle geändert werden kann. Dies steht im Gegensatz zu Variablen in anderen Sprachen, die _*veränderlich sind**, was bedeutet, dass ihre Werte jederzeit geändert werden können. Wenn Sie eine veränderliche Bindung benötigen, können Sie syntax let mutable ... verwenden.

module Immutability =

    /// Binding a value to a name via 'let' makes it immutable.
    ///
    /// The second line of code compiles, but 'number' from that point onward will shadow the previous definition.
    /// There is no way to access the previous definition of 'number' due to shadowing.
    let number = 2
    // let number = 3

    /// A mutable binding.  This is required to be able to mutate the value of 'otherNumber'.
    let mutable otherNumber = 2

    printfn $"'otherNumber' is {otherNumber}"

    // When mutating a value, use '<-' to assign a new value.
    //
    // Note that '=' is not the same as this.  Outside binding values via 'let', '=' is used to test equality.
    otherNumber <- otherNumber + 1

    printfn $"'otherNumber' changed to be {otherNumber}" 

Zahlen, booleane Werte und Zeichenfolgen

Als .NET-Sprache unterstützt F# dieselben zugrunde liegenden primitiven Typen, die in .NET vorhanden sind.

So werden verschiedene numerische Typen in F# dargestellt:

module IntegersAndNumbers = 

    /// This is a sample integer.
    let sampleInteger = 176

    /// This is a sample floating point number.
    let sampleDouble = 4.1

    /// This computed a new number by some arithmetic.  Numeric types are converted using
    /// functions 'int', 'double' and so on.
    let sampleInteger2 = (sampleInteger/4 + 5 - 7) * 4 + int sampleDouble

    /// This is a list of the numbers from 0 to 99.
    let sampleNumbers = [ 0 .. 99 ]

    /// This is a list of all tuples containing all the numbers from 0 to 99 and their squares.
    let sampleTableOfSquares = [ for i in 0 .. 99 -> (i, i*i) ]

    // The next line prints a list that includes tuples, using an interpolated string.
    printfn $"The table of squares from 0 to 99 is:\n{sampleTableOfSquares}"

Boolesche Werte und grundlegende bedingte Logik:

module Booleans =

    /// Booleans values are 'true' and 'false'.
    let boolean1 = true
    let boolean2 = false

    /// Operators on booleans are 'not', '&&' and '||'.
    let boolean3 = not boolean1 && (boolean2 || false)

    // This line uses '%b'to print a boolean value.  This is type-safe.
    printfn $"The expression 'not boolean1 && (boolean2 || false)' is %b{boolean3}"

Und so sieht die grundlegende Zeichenfolgenbearbeitung aus:

module StringManipulation = 

    /// Strings use double quotes.
    let string1 = "Hello"
    let string2  = "world"

    /// Strings can also use @ to create a verbatim string literal.
    /// This will ignore escape characters such as '\', '\n', '\t', etc.
    let string3 = @"C:\Program Files\"

    /// String literals can also use triple-quotes.
    let string4 = """The computer said "hello world" when I told it to!"""

    /// String concatenation is normally done with the '+' operator.
    let helloWorld = string1 + " " + string2 

    // This line uses '%s' to print a string value.  This is type-safe.
    printfn "%s" helloWorld

    /// Substrings use the indexer notation.  This line extracts the first 7 characters as a substring.
    /// Note that like many languages, Strings are zero-indexed in F#.
    let substring = helloWorld[0..6]
    printfn $"{substring}"

Tupel

Tupel sind in F# eine große Sache. Sie sind eine Gruppierung unbenannter, aber geordneter Werte, die selbst als Werte behandelt werden können. Stellen Sie sich diese Werte als Werte vor, die aus anderen Werten aggregiert werden. Sie haben viele Verwendungsmöglichkeiten, z. B. das bequeme Zurückgeben mehrerer Werte aus einer Funktion oder das Gruppieren von Werten für eine gewisse Ad-hoc-Vereinfachung.

module Tuples =

    /// A simple tuple of integers.
    let tuple1 = (1, 2, 3)

    /// A function that swaps the order of two values in a tuple. 
    ///
    /// F# Type Inference will automatically generalize the function to have a generic type,
    /// meaning that it will work with any type.
    let swapElems (a, b) = (b, a)

    printfn $"The result of swapping (1, 2) is {(swapElems (1,2))}"

    /// A tuple consisting of an integer, a string,
    /// and a double-precision floating point number.
    let tuple2 = (1, "fred", 3.1415)

    printfn $"tuple1: {tuple1}\ttuple2: {tuple2}"

Sie können auch struct Tupel erstellen. Diese arbeiten auch vollständig mit C#7/Visual Basic 15 Tupeln zusammen, die ebenfalls struct Tupel sind:

/// Tuples are normally objects, but they can also be represented as structs.
///
/// These interoperate completely with structs in C# and Visual Basic.NET; however,
/// struct tuples are not implicitly convertible with object tuples (often called reference tuples).
///
/// The second line below will fail to compile because of this.  Uncomment it to see what happens.
let sampleStructTuple = struct (1, 2)
//let thisWillNotCompile: (int*int) = struct (1, 2)

// Although you can
let convertFromStructTuple (struct(a, b)) = (a, b)
let convertToStructTuple (a, b) = struct(a, b)

printfn $"Struct Tuple: {sampleStructTuple}\nReference tuple made from the Struct Tuple: {(sampleStructTuple |> convertFromStructTuple)}"

Es ist wichtig zu beachten, dass Tupel, da sie Werttypen sind, nicht implizit in Verweis-Tupel konvertiert werden können struct (oder umgekehrt). Sie müssen explizit zwischen einem Verweis- und einem Struktur-Tupel konvertieren.

Pipelines

Der |> Pipeoperator wird häufig verwendet, wenn Daten in F# verarbeitet werden. Mit diesem Operator können Sie "Pipelines" von Funktionen auf flexible Weise einrichten. Das folgende Beispiel zeigt, wie Sie diese Operatoren nutzen können, um eine einfache funktionale Pipeline zu erstellen:

module PipelinesAndComposition =

    /// Squares a value.
    let square x = x * x

    /// Adds 1 to a value.
    let addOne x = x + 1

    /// Tests if an integer value is odd via modulo.
    ///
    /// '<>' is a binary comparison operator that means "not equal to".
    let isOdd x = x % 2 <> 0

    /// A list of 5 numbers.  More on lists later.
    let numbers = [ 1; 2; 3; 4; 5 ]

    /// Given a list of integers, it filters out the even numbers,
    /// squares the resulting odds, and adds 1 to the squared odds.
    let squareOddValuesAndAddOne values = 
        let odds = List.filter isOdd values
        let squares = List.map square odds
        let result = List.map addOne squares
        result

    printfn $"processing {numbers} through 'squareOddValuesAndAddOne' produces: {squareOddValuesAndAddOne numbers}"
    
    /// A shorter way to write 'squareOddValuesAndAddOne' is to nest each
    /// sub-result into the function calls themselves.
    ///
    /// This makes the function much shorter, but it's difficult to see the
    /// order in which the data is processed.
    let squareOddValuesAndAddOneNested values = 
        List.map addOne (List.map square (List.filter isOdd values))

    printfn $"processing {numbers} through 'squareOddValuesAndAddOneNested' produces: {squareOddValuesAndAddOneNested numbers}"

    /// A preferred way to write 'squareOddValuesAndAddOne' is to use F# pipe operators.
    /// This allows you to avoid creating intermediate results, but is much more readable
    /// than nesting function calls like 'squareOddValuesAndAddOneNested'
    let squareOddValuesAndAddOnePipeline values =
        values
        |> List.filter isOdd
        |> List.map square
        |> List.map addOne

    printfn $"processing {numbers} through 'squareOddValuesAndAddOnePipeline' produces: {squareOddValuesAndAddOnePipeline numbers}"

    /// You can shorten 'squareOddValuesAndAddOnePipeline' by moving the second `List.map` call
    /// into the first, using a Lambda Function.
    ///
    /// Note that pipelines are also being used inside the lambda function.  F# pipe operators
    /// can be used for single values as well.  This makes them very powerful for processing data.
    let squareOddValuesAndAddOneShorterPipeline values =
        values
        |> List.filter isOdd
        |> List.map(fun x -> x |> square |> addOne)

    printfn $"processing {numbers} through 'squareOddValuesAndAddOneShorterPipeline' produces: {squareOddValuesAndAddOneShorterPipeline numbers}"

    /// Lastly, you can eliminate the need to explicitly take 'values' in as a parameter by using '>>'
    /// to compose the two core operations: filtering out even numbers, then squaring and adding one.
    /// Likewise, the 'fun x -> ...' bit of the lambda expression is also not needed, because 'x' is simply
    /// being defined in that scope so that it can be passed to a functional pipeline.  Thus, '>>' can be used
    /// there as well.
    ///
    /// The result of 'squareOddValuesAndAddOneComposition' is itself another function which takes a
    /// list of integers as its input.  If you execute 'squareOddValuesAndAddOneComposition' with a list
    /// of integers, you'll notice that it produces the same results as previous functions.
    ///
    /// This is using what is known as function composition.  This is possible because functions in F#
    /// use Partial Application and the input and output types of each data processing operation match
    /// the signatures of the functions we're using.
    let squareOddValuesAndAddOneComposition =
        List.filter isOdd >> List.map (square >> addOne)

    printfn $"processing {numbers} through 'squareOddValuesAndAddOneComposition' produces: {squareOddValuesAndAddOneComposition numbers}"

Im vorherigen Beispiel wurden viele Features von F# verwendet, einschließlich Listenverarbeitungsfunktionen, erstklassigen Funktionen und partieller Anwendung. Obwohl es sich hierbei um erweiterte Konzepte handelt, sollte klar sein, wie einfach Funktionen zum Verarbeiten von Daten beim Erstellen von Pipelines verwendet werden können.

Listen, Arrays und Sequenzen

Listen, Arrays und Sequenzen sind drei primäre Auflistungstypen in der F#-Kernbibliothek.

Listen sind geordnete, unveränderliche Auflistungen von Elementen desselben Typs. Es handelt sich um einfach verknüpfte Listen, was bedeutet, dass sie für enumeration bestimmt sind, aber eine schlechte Wahl für zufälligen Zugriff und Verkettung, wenn sie groß sind. Dies steht im Gegensatz zu Listen in anderen gängigen Sprachen, die in der Regel keine einfach verknüpfte Liste zur Darstellung von Listen verwenden.

module Lists =

    /// Lists are defined using [ ... ].  This is an empty list.
    let list1 = [ ]  

    /// This is a list with 3 elements.  ';' is used to separate elements on the same line.
    let list2 = [ 1; 2; 3 ]

    /// You can also separate elements by placing them on their own lines.
    let list3 = [
        1
        2
        3
    ]

    /// This is a list of integers from 1 to 1000
    let numberList = [ 1 .. 1000 ]  

    /// Lists can also be generated by computations. This is a list containing 
    /// all the days of the year.
    ///
    /// 'yield' is used for on-demand evaluation. More on this later in Sequences.
    let daysList = 
        [ for month in 1 .. 12 do
              for day in 1 .. System.DateTime.DaysInMonth(2017, month) do 
                  yield System.DateTime(2017, month, day) ]

    // Print the first 5 elements of 'daysList' using 'List.take'.
    printfn $"The first 5 days of 2017 are: {daysList |> List.take 5}"

    /// Computations can include conditionals.  This is a list containing the tuples
    /// which are the coordinates of the black squares on a chess board.
    let blackSquares = 
        [ for i in 0 .. 7 do
              for j in 0 .. 7 do 
                  if (i+j) % 2 = 1 then 
                      yield (i, j) ]

    /// Lists can be transformed using 'List.map' and other functional programming combinators.
    /// This definition produces a new list by squaring the numbers in numberList, using the pipeline 
    /// operator to pass an argument to List.map.
    let squares = 
        numberList 
        |> List.map (fun x -> x*x) 

    /// There are many other list combinations. The following computes the sum of the squares of the 
    /// numbers divisible by 3.
    let sumOfSquares = 
        numberList
        |> List.filter (fun x -> x % 3 = 0)
        |> List.sumBy (fun x -> x * x)

    printfn $"The sum of the squares of numbers up to 1000 that are divisible by 3 is: %d{sumOfSquares}"

Arrays sind veränderliche Auflistungen von Elementen desselben Typs mit fester Größe. Sie unterstützen schnellen zufälligen Zugriff auf Elemente und sind schneller als F#-Listen, da sie nur zusammenhängende Speicherblöcke sind.

module Arrays =

    /// This is The empty array.  Note that the syntax is similar to that of Lists, but uses `[| ... |]` instead.
    let array1 = [| |]

    /// Arrays are specified using the same range of constructs as lists.
    let array2 = [| "hello"; "world"; "and"; "hello"; "world"; "again" |]

    /// This is an array of numbers from 1 to 1000.
    let array3 = [| 1 .. 1000 |]

    /// This is an array containing only the words "hello" and "world".
    let array4 = 
        [| for word in array2 do
               if word.Contains("l") then 
                   yield word |]

    /// This is an array initialized by index and containing the even numbers from 0 to 2000.
    let evenNumbers = Array.init 1001 (fun n -> n * 2) 

    /// Sub-arrays are extracted using slicing notation.
    let evenNumbersSlice = evenNumbers[0..500]

    /// You can loop over arrays and lists using 'for' loops.
    for word in array4 do 
        printfn $"word: {word}"

    // You can modify the contents of an array element by using the left arrow assignment operator.
    //
    // To learn more about this operator, see: https://docs.microsoft.com/dotnet/fsharp/language-reference/values/index#mutable-variables
    array2[1] <- "WORLD!"

    /// You can transform arrays using 'Array.map' and other functional programming operations.
    /// The following calculates the sum of the lengths of the words that start with 'h'.
    ///
    /// Note that in this case, similar to Lists, array2 is not mutated by Array.filter.
    let sumOfLengthsOfWords = 
        array2
        |> Array.filter (fun x -> x.StartsWith "h")
        |> Array.sumBy (fun x -> x.Length)

    printfn $"The sum of the lengths of the words in Array 2 is: %d{sumOfLengthsOfWords}"

Sequenzen sind eine logische Reihe von Elementen, die alle denselben Typ haben. Dies sind allgemeinere Typen als Listen und Arrays, die ihre "Ansicht" in einer beliebigen logischen Reihe von Elementen sein können. Sie zeichnen sich auch dadurch aus, dass sie verzögert sein können. Dies bedeutet, dass Elemente nur berechnet werden können, wenn sie benötigt werden.

module Sequences = 

    /// This is the empty sequence.
    let seq1 = Seq.empty

    /// This a sequence of values.
    let seq2 = seq { yield "hello"; yield "world"; yield "and"; yield "hello"; yield "world"; yield "again" }

    /// This is an on-demand sequence from 1 to 1000.
    let numbersSeq = seq { 1 .. 1000 }

    /// This is a sequence producing the words "hello" and "world"
    let seq3 = 
        seq { for word in seq2 do
                  if word.Contains("l") then 
                      yield word }

    /// This is a sequence producing the even numbers up to 2000.
    let evenNumbers = Seq.init 1001 (fun n -> n * 2) 

    let rnd = System.Random()

    /// This is an infinite sequence which is a random walk.
    /// This example uses yield! to return each element of a subsequence.
    let rec randomWalk x =
        seq { yield x
              yield! randomWalk (x + rnd.NextDouble() - 0.5) }

    /// This example shows the first 100 elements of the random walk.
    let first100ValuesOfRandomWalk = 
        randomWalk 5.0 
        |> Seq.truncate 100
        |> Seq.toList

    printfn $"First 100 elements of a random walk: {first100ValuesOfRandomWalk}"

Rekursive Funktionen

Die Verarbeitung von Auflistungen oder Sequenzen von Elementen erfolgt in der Regel mit Rekursion in F#. Obwohl F# Schleifen und imperative Programmierung unterstützt, wird die Rekursion bevorzugt, da die Richtigkeit einfacher garantiert werden kann.

Hinweis

Im folgenden Beispiel wird der Musterabgleich über den Ausdruck match verwendet. Dieses grundlegende Konstrukt wird weiter später in diesem Artikel behandelt.

module RecursiveFunctions = 
              
    /// This example shows a recursive function that computes the factorial of an 
    /// integer. It uses 'let rec' to define a recursive function.
    let rec factorial n = 
        if n = 0 then 1 else n * factorial (n-1)

    printfn $"Factorial of 6 is: %d{factorial 6}"

    /// Computes the greatest common factor of two integers.
    ///
    /// Since all of the recursive calls are tail calls,
    /// the compiler will turn the function into a loop,
    /// which improves performance and reduces memory consumption.
    let rec greatestCommonFactor a b =
        if a = 0 then b
        elif a < b then greatestCommonFactor a (b - a)
        else greatestCommonFactor (a - b) b

    printfn $"The Greatest Common Factor of 300 and 620 is %d{greatestCommonFactor 300 620}"

    /// This example computes the sum of a list of integers using recursion.
    ///
    /// '::' is used to split a list into the head and tail of the list,
    /// the head being the first element and the tail being the rest of the list.
    let rec sumList xs =
        match xs with
        | []    -> 0
        | y::ys -> y + sumList ys

    /// This makes 'sumList' tail recursive, using a helper function with a result accumulator.
    let rec private sumListTailRecHelper accumulator xs =
        match xs with
        | []    -> accumulator
        | y::ys -> sumListTailRecHelper (accumulator+y) ys
    
    /// This invokes the tail recursive helper function, providing '0' as a seed accumulator.
    /// An approach like this is common in F#.
    let sumListTailRecursive xs = sumListTailRecHelper 0 xs

    let oneThroughTen = [1; 2; 3; 4; 5; 6; 7; 8; 9; 10]

    printfn $"The sum 1-10 is %d{sumListTailRecursive oneThroughTen}"

F# bietet auch vollständige Unterstützung für die Optimierung von Endaufrufen. Dies ist eine Möglichkeit, rekursive Aufrufe so zu optimieren, dass sie genauso schnell sind wie ein Schleifenkonstrukt.

Aufzeichnen und Unterscheiden von Union-Typen

Datensatz- und Union-Typen sind zwei grundlegende Datentypen, die in F#-Code verwendet werden und im Allgemeinen die beste Möglichkeit sind, Daten in einem F#-Programm zu darstellen. Obwohl sie dadurch Klassen in anderen Sprachen ähneln, besteht einer ihrer Hauptunterschiede in der Semantik der strukturellen Gleichheit. Dies bedeutet, dass sie "nativ" vergleichbar sind und die Gleichheit einfach ist. Überprüfen Sie einfach, ob eine gleich der anderen ist.

Datensätze sind ein Aggregat benannter Werte mit optionalen Membern (z. B. Methoden). Wenn Sie mit C# oder Java vertraut sind, sollten diese sich ähnlich wie POCOs oder POJOs anfühlen – nur mit struktureller Gleichheit und weniger Heiterkeit.

module RecordTypes = 

    /// This example shows how to define a new record type.  
    type ContactCard = 
        { Name     : string
          Phone    : string
          Verified : bool }
              
    /// This example shows how to instantiate a record type.
    let contact1 = 
        { Name = "Alf" 
          Phone = "(206) 555-0157" 
          Verified = false }

    /// You can also do this on the same line with ';' separators.
    let contactOnSameLine = { Name = "Alf"; Phone = "(206) 555-0157"; Verified = false }

    /// This example shows how to use "copy-and-update" on record values. It creates 
    /// a new record value that is a copy of contact1, but has different values for 
    /// the 'Phone' and 'Verified' fields.
    ///
    /// To learn more, see: https://docs.microsoft.com/dotnet/fsharp/language-reference/copy-and-update-record-expressions
    let contact2 = 
        { contact1 with 
            Phone = "(206) 555-0112"
            Verified = true }

    /// This example shows how to write a function that processes a record value.
    /// It converts a 'ContactCard' object to a string.
    let showContactCard (c: ContactCard) = 
        c.Name + " Phone: " + c.Phone + (if not c.Verified then " (unverified)" else "")

    printfn $"Alf's Contact Card: {showContactCard contact1}"

    /// This is an example of a Record with a member.
    type ContactCardAlternate =
        { Name     : string
          Phone    : string
          Address  : string
          Verified : bool }

        /// Members can implement object-oriented members.
        member this.PrintedContactCard =
            this.Name + " Phone: " + this.Phone + (if not this.Verified then " (unverified)" else "") + this.Address

    let contactAlternate = 
        { Name = "Alf" 
          Phone = "(206) 555-0157" 
          Verified = false 
          Address = "111 Alf Street" }
   
    // Members are accessed via the '.' operator on an instantiated type.
    printfn $"Alf's alternate contact card is {contactAlternate.PrintedContactCard}"

Sie können Datensätze auch als Strukturen darstellen. Dies erfolgt mit dem [<Struct>] -Attribut:

[<Struct>]
type ContactCardStruct = 
    { Name     : string
      Phone    : string
      Verified : bool }

Unterscheidungs-Unions (DUs) sind Werte, bei denen es sich um eine Reihe benannter Formulare oder Fälle handelt. Im Typ gespeicherte Daten können einen von mehreren unterschiedlichen Werten haben.

module DiscriminatedUnions = 

    /// The following represents the suit of a playing card.
    type Suit = 
        | Hearts 
        | Clubs 
        | Diamonds 
        | Spades

    /// A Discriminated Union can also be used to represent the rank of a playing card.
    type Rank = 
        /// Represents the rank of cards 2 .. 10
        | Value of int
        | Ace
        | King
        | Queen
        | Jack

        /// Discriminated Unions can also implement object-oriented members.
        static member GetAllRanks() = 
            [ yield Ace
              for i in 2 .. 10 do yield Value i
              yield Jack
              yield Queen
              yield King ]
                                   
    /// This is a record type that combines a Suit and a Rank.
    /// It's common to use both Records and Discriminated Unions when representing data.
    type Card = { Suit: Suit; Rank: Rank }
              
    /// This computes a list representing all the cards in the deck.
    let fullDeck = 
        [ for suit in [ Hearts; Diamonds; Clubs; Spades] do
              for rank in Rank.GetAllRanks() do 
                  yield { Suit=suit; Rank=rank } ]

    /// This example converts a 'Card' object to a string.
    let showPlayingCard (c: Card) = 
        let rankString = 
            match c.Rank with 
            | Ace -> "Ace"
            | King -> "King"
            | Queen -> "Queen"
            | Jack -> "Jack"
            | Value n -> string n
        let suitString = 
            match c.Suit with 
            | Clubs -> "clubs"
            | Diamonds -> "diamonds"
            | Spades -> "spades"
            | Hearts -> "hearts"
        rankString  + " of " + suitString

    /// This example prints all the cards in a playing deck.
    let printAllCards() = 
        for card in fullDeck do 
            printfn $"{showPlayingCard card}"

Sie können DUs auch als Einzelfall-Unterscheidungs-Unions verwenden, um die Domänenmodellierung über primitive Typen zu unterstützen. Häufig werden Zeichenfolgen und andere primitive Typen verwendet, um etwas zu repräsentieren, und erhalten daher eine bestimmte Bedeutung. Wenn Sie jedoch nur die primitive Darstellung der Daten verwenden, kann dies dazu führen, dass fälschlicherweise ein falscher Wert zugewiesen wird. Die Darstellung der einzelnen Informationstypen als eindeutige Einzelfall-Union kann in diesem Szenario die Richtigkeit erzwingen.

// Single-case DUs are often used for domain modeling.  This can buy you extra type safety
// over primitive types such as strings and ints.
//
// Single-case DUs cannot be implicitly converted to or from the type they wrap.
// For example, a function which takes in an Address cannot accept a string as that input,
// or vice versa.
type Address = Address of string
type Name = Name of string
type SSN = SSN of int

// You can easily instantiate a single-case DU as follows.
let address = Address "111 Alf Way"
let name = Name "Alf"
let ssn = SSN 1234567890

/// When you need the value, you can unwrap the underlying value with a simple function.
let unwrapAddress (Address a) = a
let unwrapName (Name n) = n
let unwrapSSN (SSN s) = s

// Printing single-case DUs is simple with unwrapping functions.
printfn $"Address: {address |> unwrapAddress}, Name: {name |> unwrapName}, and SSN: {ssn |> unwrapSSN}"

Wie das obige Beispiel zeigt, müssen Sie den zugrunde liegenden Wert in einer Einzelfall-Unterscheidungs-Union explizit entpacken.

Darüber hinaus unterstützen DUs auch rekursive Definitionen, sodass Sie Strukturen und grundsätzlich rekursive Daten problemlos darstellen können. So können Sie beispielsweise eine Binärsuchstruktur mit den Funktionen exists und insert darstellen.

/// Discriminated Unions also support recursive definitions.
///
/// This represents a Binary Search Tree, with one case being the Empty tree,
/// and the other being a Node with a value and two subtrees.
///
/// Note 'T here is a type parameter, indicating that 'BST' is a generic type.
/// More on generics later.
type BST<'T> =
    | Empty
    | Node of value:'T * left: BST<'T> * right: BST<'T>

/// Check if an item exists in the binary search tree.
/// Searches recursively using Pattern Matching.  Returns true if it exists; otherwise, false.
let rec exists item bst =
    match bst with
    | Empty -> false
    | Node (x, left, right) ->
        if item = x then true
        elif item < x then (exists item left) // Check the left subtree.
        else (exists item right) // Check the right subtree.

/// Inserts an item in the Binary Search Tree.
/// Finds the place to insert recursively using Pattern Matching, then inserts a new node.
/// If the item is already present, it does not insert anything.
let rec insert item bst =
    match bst with
    | Empty -> Node(item, Empty, Empty)
    | Node(x, left, right) as node ->
        if item = x then node // No need to insert, it already exists; return the node.
        elif item < x then Node(x, insert item left, right) // Call into left subtree.
        else Node(x, left, insert item right) // Call into right subtree.

Da Sie mit DUs die rekursive Struktur der Struktur im Datentyp darstellen können, ist das Verwenden dieser rekursiven Struktur unkompliziert und garantiert Richtigkeit. Sie wird auch beim Musterabgleich unterstützt, wie unten dargestellt.

Musterabgleich

Musterabgleich ist das F#-Feature, das die Richtigkeit für den Betrieb mit F#-Typen ermöglicht. In den obigen Beispielen haben Sie wahrscheinlich eine ziemliche Syntax match x with ... bemerkt. Mit diesem Konstrukt kann der Compiler, der die "Form" von Datentypen verstehen kann, erzwingen, dass Sie alle möglichen Fälle berücksichtigen müssen, wenn Sie einen Datentyp über den so genannten vollständigen Musterabgleich verwenden. Dies ist für die Richtigkeit äußerst leistungsfähig und kann auf intelligente Weise verwendet werden, um das zu "heben", was normalerweise zur Laufzeit ein Problem mit der Kompilierzeit wäre.

module PatternMatching =

    /// A record for a person's first and last name
    type Person = {
        First : string
        Last  : string
    }

    /// A Discriminated Union of 3 different kinds of employees
    type Employee =
        | Engineer of engineer: Person
        | Manager of manager: Person * reports: List<Employee>
        | Executive of executive: Person * reports: List<Employee> * assistant: Employee

    /// Count everyone underneath the employee in the management hierarchy,
    /// including the employee. The matches bind names to the properties 
    /// of the cases so that those names can be used inside the match branches.
    /// Note that the names used for binding do not need to be the same as the 
    /// names given in the DU definition above.
    let rec countReports(emp : Employee) =
        1 + match emp with
            | Engineer(person) ->
                0
            | Manager(person, reports) ->
                reports |> List.sumBy countReports
            | Executive(person, reports, assistant) ->
                (reports |> List.sumBy countReports) + countReports assistant

Möglicherweise haben Sie bemerkt, dass das Muster verwendet _ wird. Dies wird als Platzhaltermuster bezeichnet,was eine Möglichkeit ist, "mir ist es egal, was etwas ist" zu sagen. Obwohl dies praktisch ist, können Sie den vollständigen Musterabgleich versehentlich umgehen und nicht mehr von Erzwingungen zur Kompilierzeit profitieren, wenn Sie bei der Verwendung von nicht vorsichtig _ sind. Sie wird am besten verwendet, wenn bestimmte Teile eines zersetzten Typs beim Musterabgleich nicht wichtig sind, oder die letzte Klausel, wenn Sie alle aussagekräftigen Fälle in einem Musterabgleichsausdruck aufzählen.

Im folgenden Beispiel wird der Fall verwendet, _ wenn ein Analysevorgang fehlschlägt.

/// Find all managers/executives named "Dave" who do not have any reports.
/// This uses the 'function' shorthand to as a lambda expression.
let findDaveWithOpenPosition(emps : List<Employee>) =
    emps
    |> List.filter(function
                   | Manager({First = "Dave"}, []) -> true // [] matches an empty list.
                   | Executive({First = "Dave"}, [], _) -> true
                   | _ -> false) // '_' is a wildcard pattern that matches anything.
                                 // This handles the "or else" case.

/// You can also use the shorthand function construct for pattern matching, 
/// which is useful when you're writing functions which make use of Partial Application.
let private parseHelper (f: string -> bool * 'T) = f >> function
    | (true, item) -> Some item
    | (false, _) -> None

let parseDateTimeOffset = parseHelper DateTimeOffset.TryParse

let result = parseDateTimeOffset "1970-01-01"
match result with
| Some dto -> printfn "It parsed!"
| None -> printfn "It didn't parse!"

// Define some more functions which parse with the helper function.
let parseInt = parseHelper Int32.TryParse
let parseDouble = parseHelper Double.TryParse
let parseTimeSpan = parseHelper TimeSpan.TryParse

Aktive Muster sind ein weiteres leistungsstarkes Konstrukt, das mit Musterabgleich verwendet werden kann. Sie ermöglichen es Ihnen, Eingabedaten in benutzerdefinierte Formulare zu partitionieren und sie an der Aufrufsite für Musterkonformität zu zersetzen. Sie können auch parametrisiert werden, sodass die Partition als Funktion definiert werden kann. Das Erweitern des vorherigen Beispiels zur Unterstützung von aktiven Mustern sieht in etwa wie hier aus:

let (|Int|_|) = parseInt
let (|Double|_|) = parseDouble
let (|Date|_|) = parseDateTimeOffset
let (|TimeSpan|_|) = parseTimeSpan

/// Pattern Matching via 'function' keyword and Active Patterns often looks like this.
let printParseResult = function
    | Int x -> printfn $"%d{x}"
    | Double x -> printfn $"%f{x}"
    | Date d -> printfn $"%O{d}"
    | TimeSpan t -> printfn $"%O{t}"
    | _ -> printfn "Nothing was parse-able!"

// Call the printer with some different values to parse.
printParseResult "12"
printParseResult "12.045"
printParseResult "12/28/2016"
printParseResult "9:01PM"
printParseResult "banana!"

Optionen

Ein Sonderfall von Unterscheidungs-Union-Typen ist der Optionstyp, der so nützlich ist, dass er Teil der F#-Kernbibliothek ist.

Der Optionstyp ist ein Typ, der einen von zwei Fällen darstellt: einen Wert oder gar nichts. Sie wird in jedem Szenario verwendet, in dem ein Wert aus einem bestimmten Vorgang resultiert oder nicht. Dies zwingt Sie dann, beide Fälle zu berücksichtigen, wodurch dies zu einem Problem zur Kompilierzeit und nicht zu einem Laufzeitanliegen wird. Diese werden häufig in APIs verwendet, bei denen stattdessen verwendet wird, um "nichts" zu repräsentieren, sodass es in vielen Fällen nicht mehr notwendig ist, sich null Gedanken zu NullReferenceException machen.

module OptionValues = 

    /// First, define a zip code defined via Single-case Discriminated Union.
    type ZipCode = ZipCode of string

    /// Next, define a type where the ZipCode is optional.
    type Customer = { ZipCode: ZipCode option }

    /// Next, define an interface type that represents an object to compute the shipping zone for the customer's zip code, 
    /// given implementations for the 'getState' and 'getShippingZone' abstract methods.
    type IShippingCalculator =
        abstract GetState : ZipCode -> string option
        abstract GetShippingZone : string -> int

    /// Next, calculate a shipping zone for a customer using a calculator instance.
    /// This uses combinators in the Option module to allow a functional pipeline for
    /// transforming data with Optionals.
    let CustomerShippingZone (calculator: IShippingCalculator, customer: Customer) =
        customer.ZipCode 
        |> Option.bind calculator.GetState 
        |> Option.map calculator.GetShippingZone

Maßeinheiten

Das Typsystem von F# bietet die Möglichkeit, Kontext für numerische Literale über Measureeinheiten (Units of Measure) zur Verfügung zu stellen. Mit Maßeinheiten können Sie einer Einheit einen numerischen Typ zuordnen, z. B. Meter, und Funktionen können Aufgaben für Einheiten und nicht für numerische Literale ausführen. Dadurch kann der Compiler überprüfen, ob die typen der übergebenen numerischen Literale unter einem bestimmten Kontext sinnvoll sind, wodurch Laufzeitfehler im Zusammenhang mit dieser Art von Arbeit beseitigt werden.

module UnitsOfMeasure = 

    /// First, open a collection of common unit names
    open Microsoft.FSharp.Data.UnitSystems.SI.UnitNames

    /// Define a unitized constant
    let sampleValue1 = 1600.0<meter>          

    /// Next, define a new unit type
    [<Measure>]
    type mile =
        /// Conversion factor mile to meter.
        static member asMeter = 1609.34<meter/mile>

    /// Define a unitized constant
    let sampleValue2  = 500.0<mile>          

    /// Compute  metric-system constant
    let sampleValue3 = sampleValue2 * mile.asMeter   

    // Values using Units of Measure can be used just like the primitive numeric type for things like printing.
    printfn $"After a %f{sampleValue1} race I would walk %f{sampleValue2} miles which would be %f{sampleValue3} meters"

Die F#-Kernbibliothek definiert viele SI-Einheitentypen und Einheitenkonvertierungen. Weitere Informationen finden Sie im Namespace FSharp.Data.UnitSystems.SI.UnitSymbols.

Objektprogrammierung

F# bietet vollständige Unterstützung für die Objektprogrammierung über Klassen, Schnittstellen, abstrakte Klassen, Vererbungusw.

Klassen sind Typen, die .NET-Objekte darstellen, die Eigenschaften, Methoden und Ereignisse als Memberaufweisen können.

module DefiningClasses = 

    /// A simple two-dimensional Vector class.
    ///
    /// The class's constructor is on the first line,
    /// and takes two arguments: dx and dy, both of type 'double'.
    type Vector2D(dx : double, dy : double) =

        /// This internal field stores the length of the vector, computed when the 
        /// object is constructed
        let length = sqrt (dx*dx + dy*dy)

        // 'this' specifies a name for the object's self-identifier.
        // In instance methods, it must appear before the member name.
        member this.DX = dx

        member this.DY = dy

        member this.Length = length

        /// This member is a method.  The previous members were properties.
        member this.Scale(k) = Vector2D(k * this.DX, k * this.DY)
    
    /// This is how you instantiate the Vector2D class.
    let vector1 = Vector2D(3.0, 4.0)

    /// Get a new scaled vector object, without modifying the original object.
    let vector2 = vector1.Scale(10.0)

    printfn $"Length of vector1: %f{vector1.Length}\nLength of vector2: %f{vector2.Length}"

Das Definieren generischer Klassen ist ebenfalls einfach.

module DefiningGenericClasses = 

    type StateTracker<'T>(initialElement: 'T) = 

        /// This internal field store the states in a list.
        let mutable states = [ initialElement ]

        /// Add a new element to the list of states.
        member this.UpdateState newState = 
            states <- newState :: states  // use the '<-' operator to mutate the value.

        /// Get the entire list of historical states.
        member this.History = states

        /// Get the latest state.
        member this.Current = states.Head

    /// An 'int' instance of the state tracker class. Note that the type parameter is inferred.
    let tracker = StateTracker 10

    // Add a state
    tracker.UpdateState 17

Um eine Schnittstelle zu implementieren, können Sie entweder interface ... with die Syntax oder einen Objektausdruckverwenden.

module ImplementingInterfaces =

    /// This is a type that implements IDisposable.
    type ReadFile() =

        let file = new System.IO.StreamReader("readme.txt")

        member this.ReadLine() = file.ReadLine()

        // This is the implementation of IDisposable members.
        interface System.IDisposable with
            member this.Dispose() = file.Close()


    /// This is an object that implements IDisposable via an Object Expression
    /// Unlike other languages such as C# or Java, a new type definition is not needed 
    /// to implement an interface.
    let interfaceImplementation =
        { new System.IDisposable with
            member this.Dispose() = printfn "disposed" }

Welche Typen verwendet werden sollen

Das Vorhandensein von Klassen, Datensätzen, Unterscheidungs-Unions und Tupeln führt zu einer wichtigen Frage: Welche sollten Sie verwenden? Wie die meisten Anderen im Leben hängt die Antwort von Ihren Umständen ab.

Tupel eignen sich hervorragend zum Zurückgeben mehrerer Werte aus einer Funktion und zum Verwenden eines Ad-hoc-Aggregats von Werten als Wert selbst.

Datensätze sind ein "Schritt nach oben" von Tupeln mit benannten Bezeichnungen und Unterstützung für optionale Member. Sie eignen sich hervorragend für eine niedrige Darstellung von Daten während der Übertragung durch Ihr Programm. Da sie strukturell gleich sind, sind sie einfach beim Vergleich zu verwenden.

Unterscheidungs-Unions haben viele Verwendungsmöglichkeiten, aber der Hauptvorteil besteht darin, sie in Verbindung mit dem Musterabgleich nutzen zu können, um alle möglichen "Formen" zu berücksichtigen, die daten haben können.

Klassen eignen sich hervorragend für eine Vielzahl von Gründen, z. B. wenn Sie Informationen darstellen und diese Informationen an die Funktionalität binden müssen. Als Faustregel gilt: Wenn Sie über Funktionen verfügen, die konzeptionell an einige Daten gebunden sind, ist die Verwendung von Klassen und die Prinzipien der Object-Oriented-Programmierung ein wesentlicher Vorteil. Klassen sind auch der bevorzugte Datentyp bei der Zusammenarbeit mit C# und Visual Basic, da diese Sprachen Klassen für nahezu alles verwenden.

Nächste Schritte

Nachdem Sie nun einige der wichtigsten Features der Sprache gesehen haben, sollten Sie bereit sein, Ihre ersten F#-Programme zu schreiben! Sehen Sie sich Erste Schritte an, um zu erfahren, wie Sie Ihre Entwicklungsumgebung einrichten und Code schreiben.

Sehen Sie sich auch die F#-Sprachreferenz an, um eine umfassende Sammlung von konzeptionellen Inhalten zu F# anzuzeigen.