Présentation de F#
La meilleure façon d’en savoir plus sur F# est de lire et d’écrire du code F#. Cet article fera office de visite guidée de certaines des principales fonctionnalités de F# et vous donnera des extraits de code que vous pouvez exécuter sur votre ordinateur. Pour en savoir plus sur la configuration d’un environnement de développement, consultez Précis.
Il existe deux concepts principaux dans F# : les fonctions et les types. Cette visite guidée met l’accent sur les caractéristiques du langage qui entrent dans ces deux concepts.
Exécution du code en ligne
Si vous n’avez pas installé F# sur votre ordinateur, vous pouvez exécuter tous les exemples de votre navigateur avec Essayer F# dans Fable. Fable est un dialecte de F# qui s’exécute directement dans votre navigateur. Pour afficher les exemples qui suivent dans le REPL, consultez Samples > Learn > Tour of F# dans la barre de menus de gauche du REPL Fable.
Fonctions et modules
Les éléments les plus fondamentaux d’un programme F# sont fonctions organisées en modules . Les fonctions effectuent un travail sur des entrées pour produire des sorties. Elles sont organisées en modules, qui sont le principal moyen de regrouper des éléments en F#. Ils sont définis à l’aide de la liaison let
, qui donnent à la fonction un nom et définissent ses arguments.
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}"
Les liaisons let
permettent également de lier une valeur à un nom, à l’instar des variables dans d’autres langages. let
liaisons sont immuables par défaut, ce qui signifie qu’une fois qu’une valeur ou une fonction est liée à un nom, elle ne peut pas être modifiée sur place. Contrairement aux variables dans d’autres langages, qui sont mutables, ce qui signifie que leurs valeurs peuvent être modifiées à tout moment. Si vous avez besoin d'une liaison mutable, vous pouvez utiliser la syntaxe let mutable ...
.
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}"
Nombres, booléens et chaînes
En tant que langage .NET, F# prend en charge les mêmes types primitifs sous-jacents qui existent dans .NET.
Voici comment les différents types numériques sont représentés en F# :
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}"
Voici les valeurs booléennes et l’exécution d’une logique conditionnelle de base :
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}"
Et voici à quoi ressemble la manipulation de base des chaînes :
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}"
Tuples
Les tuples ont beaucoup d’importance en F#. Il s’agit d’un regroupement de valeurs non nommées mais ordonnées qui peuvent être traitées comme des valeurs elles-mêmes. Considérez-les comme des valeurs agrégées à partir d’autres valeurs. Ils ont de nombreuses utilisations, telles que le renvoi pratique de plusieurs valeurs à partir d’une fonction ou le regroupement de valeurs pour une commodité ad hoc.
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}"
Vous pouvez également créer des tuples struct
. Ceux-ci interagissent également entièrement avec les tuples de C#7/Visual Basic 15, qui sont aussi des tuples struct
:
/// 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)}"
Il est important de noter que, étant donné que les tuples struct
sont des types valeur, ils ne peuvent pas être convertis implicitement en tuples de référence ou inversement. Vous devez effectuer une conversion explicite entre un tuple de référence et un tuple struct.
Pipelines
L’opérateur de canal |>
est utilisé en grande partie lors du traitement des données en F#. Cet opérateur vous permet d’établir des « pipelines » de fonctions de manière flexible. L’exemple suivant explique comment tirer parti de ces opérateurs pour créer un pipeline fonctionnel simple :
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}"
L’exemple précédent met en avant plusieurs fonctionnalités de F#, notamment les fonctions de traitement de liste, les fonctions de première classe et l’application partielle. Bien que ces concepts soient avancés, il doit être clair comment les fonctions peuvent être facilement utilisées pour traiter les données lors de la création de pipelines.
Listes, tableaux et séquences
Les listes, les tableaux et les séquences sont trois types de collection principaux dans la bibliothèque principale F#.
Les listes sont des collections ordonnées et immuables d’éléments du même type. Les listes étant liées individuellement, elles sont destinées à l’énumération et ne sont pas indiquées pour l’accès aléatoire et la concaténation si elles sont volumineuses. Ceci contraste avec les listes dans d’autres langages populaires qui n’utilisent généralement pas de liste liée individuellement pour la représentation de listes.
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}"
Les tableaux sont des collections de taille fixe et mutables d’éléments du même type. Ils prennent en charge l’accès aléatoire rapide des éléments et sont plus rapides que les listes F#, car ils sont simplement des blocs contigus de mémoire.
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://learn.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}"
Sequences sont une série logique d’éléments, tous de même type. Il s’agit d’un type plus général que les listes et les tableaux, capables d’être votre « affichage » dans n’importe quelle série logique d’éléments. Ils se distinguent également parce qu’ils peuvent être paresseux, ce qui signifie que les éléments peuvent être calculés uniquement quand ils sont nécessaires.
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}"
Fonctions récursives
Le traitement de collections ou de séquences d’éléments est généralement effectué de manière récursive en F#. Bien que F# ait pris en charge les boucles et la programmation impérative, la récursivité est préférée, car il est plus facile de garantir l’exactitude.
Remarque
L’exemple suivant utilise le filtrage par motif via l’expression match
. Cette construction fondamentale est abordée plus loin dans cet article.
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# prend également entièrement en charge l’optimisation de la récursion terminale (« tail call optimization »), qui est un moyen d’optimiser les appels récursifs afin qu’ils soient aussi rapides qu’une construction en boucle.
Types Enregistrement et Union discriminée
Les types d’enregistrement et d’union sont deux types de données fondamentaux utilisés dans le code F#, et sont généralement le meilleur moyen de représenter des données dans un programme F#. Bien que cela les rend similaires aux classes dans d’autres langues, l’une de leurs principales différences est qu’elles ont une sémantique d’égalité structurelle. Cela signifie qu’ils sont « en mode natif » comparable et que l’égalité est simple : vérifiez simplement si l’un est égal à l’autre.
Records sont un agrégat de valeurs nommées, avec des membres facultatifs (comme des méthodes). Si vous êtes familiarisé avec C# ou Java, ceux-ci devraient se révéler similaires aux POCOs ou POJOs, mais avec égalité de structure et moins de formalisme.
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://learn.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}"
Vous pouvez également représenter des enregistrements sous forme de structures. Pour ce faire, utilisez l’attribut [<Struct>]
:
[<Struct>]
type ContactCardStruct =
{ Name : string
Phone : string
Verified : bool }
Les unions discriminées (DU) sont des valeurs qui peuvent correspondre à un certain nombre de formulaires ou de cas nommés. Les données stockées dans le type peuvent être l'une des multiples valeurs distinctes.
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}"
Vous pouvez également utiliser des DU en tant qu’unions discriminées à cas unique pour faciliter la modélisation de domaine sur des types primitifs. Souvent, les chaînes et d’autres types primitifs sont utilisés pour représenter quelque chose, et sont donc donné une signification particulière. Toutefois, l’utilisation uniquement de la représentation primitive des données peut entraîner l’affectation erronée d’une valeur incorrecte ! La représentation de chaque type d'information comme une union distincte à cas unique peut assurer la conformité dans ce scénario.
// 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}"
Comme le montre l’exemple ci-dessus, pour obtenir la valeur sous-jacente dans une union discriminée à cas unique, vous devez la désenvelopper explicitement.
Par ailleurs, les DU prennent également en charge les définitions récursives, ce qui vous permet de représenter facilement des arborescences et des données intrinsèquement récursives. Par exemple, voici comment représenter une arborescence de recherche binaire avec des fonctions exists
et insert
.
/// 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.
Étant donné que les DUs vous permettent de représenter la structure récursive de l’arborescence dans le type de données, travailler avec cette structure récursive est simple et assure la justesse. Elles sont également prises en charge dans les critères spéciaux, comme indiqué ci-dessous.
Critères spéciaux
Les critères spéciaux sont une fonctionnalité F# qui permet de garantir l’exactitude lors de l’utilisation de types F#. Dans les exemples ci-dessus, vous avez probablement remarqué un peu de syntaxe match x with ...
. Cette construction permet au compilateur, qui peut comprendre la « forme » des types de données, de vous forcer à prendre en compte tous les cas possibles lors de l’utilisation d’un type de données par le biais de ce qu’on appelle la correspondance exhaustive des modèles. C’est incroyablement puissant pour l’exactitude et peut être utilisé intelligemment pour transposer ce qui serait normalement une préoccupation en temps d’exécution en une préoccupation en temps de compilation.
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
L’exemple suivant présente le modèle _
, que vous avez peut-être remarqué précédemment. Ce modèle, appelé modèle générique, est une façon de dire « je me fiche de savoir ce que c’est ». Bien que ce modèle soit pratique, vous pouvez accidentellement ignorer les critères spéciaux exhaustifs et ne plus bénéficier de mises en application au moment de la compilation si vous utilisez _
sans faire attention. Il est préférable d’utiliser quand vous ne vous souciez pas de certains éléments d’un type décomposé lors de la mise en correspondance de modèles, ou de la clause finale lorsque vous avez énuméré tous les cas significatifs dans une expression de correspondance de modèle.
Dans l’exemple suivant, le cas de _
est utilisé lorsqu’une opération d’analyse échoue.
/// 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
Les modèles actifs sont une autre construction puissante que vous pouvez utiliser avec les critères spéciaux. Ils permettent de partitionner des données d’entrée dans des formulaires personnalisés en les décomposant au niveau du site d’appel des critères spéciaux. Ils peuvent également être paramétrés, ce qui permet de définir la partition en tant que fonction. L’extension de l’exemple précédent pour prendre en charge les modèles actifs ressemble à ceci :
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!"
Options
Un cas particulier des types d'union discriminée est le type option, qui est tellement utile qu’il fait partie de la bibliothèque principale F#.
Le type d’option est un type qui représente l’un des deux cas suivants : une valeur ou rien du tout. Il est utilisé dans n’importe quel scénario où une valeur peut ou non résulter d’une opération particulière. Cela vous oblige ensuite à prendre en compte les deux cas, ce qui en fait une préoccupation au moment de la compilation plutôt qu’une préoccupation d’exécution. Ceux-ci sont souvent utilisés dans les API où null
est utilisé pour représenter « rien » à la place, éliminant ainsi la nécessité de se soucier de NullReferenceException
dans de nombreuses circonstances.
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
Unités de mesure
Le système de type de F# offre la possibilité de fournir un contexte pour les littéraux numériques par le biais d’unités de mesure. Les unités de mesure vous permettent d’associer un type numérique à une unité, comme Les compteurs, et d’effectuer des fonctions sur des unités plutôt que sur des littéraux numériques. Cela permet au compilateur de vérifier que les types de littéraux numériques passés sont logiques dans un certain contexte, ce qui élimine ainsi les erreurs d’exécution associées à ce type de travail.
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"
La bibliothèque F# Core définit de nombreux types d’unités SI et conversions d’unités. Pour plus d’informations, consultez l’espace de noms FSharp.Data.UnitSystems.SI.UnitSymbols.
Programmation d’objets
F# prend entièrement en charge la programmation d’objets par le biais de classes, d’interfaces, de classes abstraites, de relations d’héritage, etc.
Les classes sont des types représentant des objets .NET, qui peuvent avoir des propriétés, des méthodes et des événements comme membres.
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}"
La définition de classes génériques est également simple.
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
Pour implémenter une interface, vous pouvez utiliser la syntaxe interface ... with
ou une expression d’objet .
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" }
Types à utiliser
La présence de classes, d’enregistrements, d’unions discriminées et de tuples conduit à une question importante : quel type utiliser ? Comme la plupart des choses dans la vie, la réponse dépend de vos circonstances.
Les tuples sont parfaits pour retourner plusieurs valeurs à partir d’une fonction et utiliser un agrégat ad hoc de valeurs comme valeur elle-même.
Un enregistrement est un tuple « amélioré », avec des étiquettes nommées et la prise en charge de membres facultatifs. Ils sont parfaits pour une représentation des données en transit dans votre programme avec moins de cérémonie. Étant donné qu’ils ont une égalité structurelle, ils sont faciles à utiliser avec la comparaison.
Les unions discriminantes ont de nombreuses utilisations, mais l'avantage principal est de pouvoir les utiliser conjointement avec le Pattern Matching pour tenir compte de toutes les « formes » possibles qu'une donnée peut avoir.
Les classes sont idéales pour un grand nombre de raisons, telles que lorsque vous avez besoin de représenter des informations et liez également ces informations à des fonctionnalités. En règle générale, lorsque vous disposez de fonctionnalités liées conceptuellement à certaines données, l’utilisation de classes et les principes de Object-Oriented Programmation est un avantage significatif. Les classes sont également le type de données préféré lors de l’interopérabilité avec C# et Visual Basic, car ces langages utilisent des classes pour presque tout.
Étapes suivantes
Maintenant que vous avez vu certaines des principales fonctionnalités du langage, vous devez être prêt à écrire vos premiers programmes F# ! Découvrez "Démarrage" pour apprendre à configurer votre environnement de développement et à écrire du code.
Consultez également les informations de référence sur le langage F# pour voir une collection complète de contenus conceptuels sur F#.