Nouveautés de F# 5

F# 5 ajoute plusieurs améliorations au langage F# et à F# Interactive. Il est publié avec .NET 5.

Vous pouvez télécharger le dernier SDK .NET sur la page de téléchargements .NET.

Bien démarrer

F# 5 est disponible dans toutes les distributions .NET Core et les outils Visual Studio. Pour plus d’informations, consultez Prise en main de F#.

Références de package dans les scripts F#

F# 5 prend en charge les références de package dans les scripts F# avec la syntaxe #r "nuget:...". Par exemple, considérez la référence de package suivante :

#r "nuget: Newtonsoft.Json"

open Newtonsoft.Json

let o = {| X = 2; Y = "Hello" |}

printfn $"{JsonConvert.SerializeObject o}"

Vous pouvez également fournir une version explicite après le nom du package comme suit :

#r "nuget: Newtonsoft.Json,11.0.1"

Les références de package prennent en charge les packages avec des dépendances natives, telles que ML.NET.

Les références de package prennent également en charge les packages avec des spécifications particulières concernant le référencement des bibliothèques .dll dépendantes. Par exemple, le package FParsec utilisé pour exiger des utilisateurs qu’ils vérifient manuellement que leur bibliothèque FParsecCS.dll dépendante a été référencée en premier avant le référencement de FParsec.dll dans F# Interactive. Cela n’est plus nécessaire et vous pouvez référencer le package comme suit :

#r "nuget: FParsec"

open FParsec

let test p str =
    match run p str with
    | Success(result, _, _)   -> printfn $"Success: {result}"
    | Failure(errorMsg, _, _) -> printfn $"Failure: {errorMsg}"

test pfloat "1.234"

Cette fonctionnalité implémente le document F# Tooling RFC FST-1027. Pour plus d’informations sur les références de package, consultez le didacticiel sur F# Interactive.

Interpolation de chaîne

Les chaînes interpolées F# sont assez similaires aux chaînes interpolées C# ou JavaScript, car elles vous permettent d’écrire du code dans des « trous » à l’intérieur d’un littéral de chaîne. Voici un exemple de base :

let name = "Phillip"
let age = 29
printfn $"Name: {name}, Age: {age}"

printfn $"I think {3.0 + 0.14} is close to {System.Math.PI}!"

Toutefois, les chaînes interpolées F# autorisent également les interpolations typées, tout comme la fonction sprintf, pour appliquer la nécessité pour une expression à l’intérieur d’un contexte interpolé de se conformer à un type particulier. Il utilise les mêmes spécificateurs de format.

let name = "Phillip"
let age = 29

printfn $"Name: %s{name}, Age: %d{age}"

// Error: type mismatch
printfn $"Name: %s{age}, Age: %d{name}"

Dans l’exemple d’interpolation typée précédent, le %s requiert que l’interpolation soit de type string, tandis que le %d requiert que l’interpolation soit un integer.

En outre, des expressions F# arbitraires peuvent être placées dans un contexte d’interpolation. Il est même possible d’écrire une expression plus complexe, comme suit :

let str =
    $"""The result of squaring each odd item in {[1..10]} is:
{
    let square x = x * x
    let isOdd x = x % 2 <> 0
    let oddSquares xs =
        xs
        |> List.filter isOdd
        |> List.map square
    oddSquares [1..10]
}
"""

Toutefois, dans la pratique, nous ne recommandons pas de le faire trop souvent.

Cette fonctionnalité implémente le document F# RFC FS-1001.

Prise en charge de nameof

F# 5 prend en charge l’opérateur nameof, qui résout le symbole pour lequel il est utilisé et produit son nom dans la source F#. Cela s’avère utile dans différents scénarios, tels que la journalisation et permet de protéger votre journalisation contre les changements au niveau du code source.

let months =
    [
        "January"; "February"; "March"; "April";
        "May"; "June"; "July"; "August"; "September";
        "October"; "November"; "December"
    ]

let lookupMonth month =
    if (month > 12 || month < 1) then
        invalidArg (nameof month) (sprintf "Value passed in was %d." month)

    months[month-1]

printfn $"{lookupMonth 12}"
printfn $"{lookupMonth 1}"
printfn $"{lookupMonth 13}"

La dernière ligne lève une exception et « mois » s’affiche dans le message d’erreur.

Vous pouvez prendre le nom de presque chaque construction F# :

module M =
    let f x = nameof x

printfn $"{M.f 12}"
printfn $"{nameof M}"
printfn $"{nameof M.f}"

Trois ajouts finaux sont des modifications apportées au fonctionnement des opérateurs : l’ajout de la forme nameof<'type-parameter> pour les paramètres de type générique et la possibilité d’utiliser nameof comme modèle dans une expression de critères spéciaux.

L’extraction du nom d’un opérateur permet de trouver sa chaîne source. Si vous avez besoin de la forme compilée, utilisez le nom compilé d’un opérateur :

nameof(+) // "+"
nameof op_Addition // "op_Addition"

L’extraction du nom d’un paramètre de type nécessite une syntaxe légèrement différente :

type C<'TType> =
    member _.TypeName = nameof<'TType>

Ceci est similaire aux opérateurs typedefof<'T> et typeof<'T>.

F# 5 ajoute également la prise en charge d’un modèle nameof qui peut être utilisé dans les expressions match :

[<Struct; IsByRefLike>]
type RecordedEvent = { EventType: string; Data: ReadOnlySpan<byte> }

type MyEvent =
    | AData of int
    | BData of string

let deserialize (e: RecordedEvent) : MyEvent =
    match e.EventType with
    | nameof AData -> AData (JsonSerializer.Deserialize<int> e.Data)
    | nameof BData -> BData (JsonSerializer.Deserialize<string> e.Data)
    | t -> failwithf "Invalid EventType: %s" t

Le code précédent utilise « nameof » au lieu du littéral de chaîne dans l’expression de correspondance.

Cette fonctionnalité implémente le document F# RFC FS-1003.

Déclarations de type ouvert

F# 5 ajoute également la prise en charge des déclarations de type ouvert. Une déclaration de type ouvert est similaire à l’ouverture d’une classe statique en C#, à l’exception d’une syntaxe différente et d’un comportement légèrement différent compatible avec la sémantique F#.

Avec les déclarations de type ouvert, vous pouvez open n’importe quel type pour exposer du contenu statique à l’intérieur de celui-ci. En outre, vous pouvez open des unions et enregistrements définis par F# pour exposer leur contenu. Par exemple, cela peut être utile si vous avez une union définie dans un module et souhaitez accéder à ses cas, mais ne souhaitez pas ouvrir l’intégralité du module.

open type System.Math

let x = Min(1.0, 2.0)

module M =
    type DU = A | B | C

    let someOtherFunction x = x + 1

// Open only the type inside the module
open type M.DU

printfn $"{A}"

Contrairement à C#, lorsque vous utilisez open type sur deux types qui exposent un membre portant le même nom, le membre du dernier type sur lequel open est utilisé occulte l’autre nom. Cette pratique est cohérente avec la sémantique F# autour de l’occultation qui existe déjà.

Cette fonctionnalité implémente le document F# RFC FS-1068.

Comportement de découpage cohérent pour les types de données intégrés

Un comportement pour le découpage des types de données FSharp.Core intégrés (tableau, liste, chaîne, tableau 2D, tableau 3D, tableau 4D) est utilisé pour ne pas présenter de cohérence avant F# 5. Certains comportements de cas limite levaient une exception et d’autres non. En F# 5, tous les types intégrés retournent désormais des tranches vides pour les tranches impossibles à générer :

let l = [ 1..10 ]
let a = [| 1..10 |]
let s = "hello!"

// Before: would return empty list
// F# 5: same
let emptyList = l[-2..(-1)]

// Before: would throw exception
// F# 5: returns empty array
let emptyArray = a[-2..(-1)]

// Before: would throw exception
// F# 5: returns empty string
let emptyString = s[-2..(-1)]

Cette fonctionnalité implémente le document F# RFC FS-1077.

Tranches d’index fixe pour les tableaux 3D et 4D dans FSharp.Core

F# 5 prend en charge le découpage avec un index fixe dans les types 3D et 4D intégrés.

Pour illustrer cela, considérez le tableau 3D suivant :

z = 0

x\y 0 1
0 0 1
1 2 3

z = 1

x\y 0 1
0 4 5
1 6 7

Que faire si vous souhaitez extraire la tranche [| 4; 5 |] du tableau ? C’est désormais très simple.

// First, create a 3D array to slice

let dim = 2
let m = Array3D.zeroCreate<int> dim dim dim

let mutable count = 0

for z in 0..dim-1 do
    for y in 0..dim-1 do
        for x in 0..dim-1 do
            m[x,y,z] <- count
            count <- count + 1

// Now let's get the [4;5] slice!
m[*, 0, 1]

Cette fonctionnalité implémente le document F# RFC FS-1077b.

Améliorations des quotations F#

Les quotations de code F# ont désormais la possibilité de conserver les informations de contrainte de type. Prenons l’exemple suivant :

open FSharp.Linq.RuntimeHelpers

let eval q = LeafExpressionConverter.EvaluateQuotation q

let inline negate x = -x
// val inline negate: x: ^a ->  ^a when  ^a : (static member ( ~- ) :  ^a ->  ^a)

<@ negate 1.0 @>  |> eval

La contrainte générée par la fonction inline est conservée dans la quotation de code. La forme faisant l’objet d’une quotation de la fonction negate peut désormais être évaluée.

Cette fonctionnalité implémente le document F# RFC FS-1071.

Expressions de calcul applicatives

Les expressions de calcul sont utilisées aujourd’hui pour modéliser des « calculs contextuels » ou dans des calculs monadiques à la terminologie adaptée à la programmation plus fonctionnels.

F# 5 introduit des expressions de calcul applicatives, qui offrent un autre modèle de calcul. Les expressions de calcul applicatives permettent des calculs plus efficaces, à condition que chaque calcul soit indépendant et que leurs résultats soient accumulés à la fin. Lorsque les calculs sont indépendants les uns des autres, ils sont également facilement parallélisables, ce qui permet aux auteurs d’expressions de calcul d’écrire des bibliothèques plus efficaces. Cet avantage est toutefois lié à une restriction : les calculs qui dépendent de valeurs précédemment calculées ne sont pas autorisés.

L’exemple suivant montre une expression de calcul applicative de base pour le type Result.

// First, define a 'zip' function
module Result =
    let zip x1 x2 =
        match x1,x2 with
        | Ok x1res, Ok x2res -> Ok (x1res, x2res)
        | Error e, _ -> Error e
        | _, Error e -> Error e

// Next, define a builder with 'MergeSources' and 'BindReturn'
type ResultBuilder() =
    member _.MergeSources(t1: Result<'T,'U>, t2: Result<'T1,'U>) = Result.zip t1 t2
    member _.BindReturn(x: Result<'T,'U>, f) = Result.map f x

let result = ResultBuilder()

let run r1 r2 r3 =
    // And here is our applicative!
    let res1: Result<int, string> =
        result {
            let! a = r1
            and! b = r2
            and! c = r3
            return a + b - c
        }

    match res1 with
    | Ok x -> printfn $"{nameof res1} is: %d{x}"
    | Error e -> printfn $"{nameof res1} is: {e}"

let printApplicatives () =
    let r1 = Ok 2
    let r2 = Ok 3 // Error "fail!"
    let r3 = Ok 4

    run r1 r2 r3
    run r1 (Error "failure!") r3

Si vous êtes un auteur de bibliothèque qui expose des expressions de calcul dans sa bibliothèque aujourd’hui, vous devez tenir compte de certaines considérations supplémentaires.

Cette fonctionnalité implémente le document F# RFC FS-1063.

Les interfaces peuvent être implémentées au niveau de différentes instanciations génériques

Vous pouvez maintenant implémenter la même interface au niveau de différentes instanciations génériques :

type IA<'T> =
    abstract member Get : unit -> 'T

type MyClass() =
    interface IA<int> with
        member x.Get() = 1
    interface IA<string> with
        member x.Get() = "hello"

let mc = MyClass()
let iaInt = mc :> IA<int>
let iaString = mc :> IA<string>

iaInt.Get() // 1
iaString.Get() // "hello"

Cette fonctionnalité implémente le document F# RFC FS-1031.

Consommation par les membres de l’interface par défaut

F# 5 vous permet de consommer des interfaces avec des implémentations par défaut.

Considérez une interface définie en C# comme suit :

using System;

namespace CSharp
{
    public interface MyDim
    {
        public int Z => 0;
    }
}

Vous pouvez l’utiliser en F# via l’un des moyens standard d’implémentation d’une interface :

open CSharp

// You can implement the interface via a class
type MyType() =
    member _.M() = ()

    interface MyDim

let md = MyType() :> MyDim
printfn $"DIM from C#: %d{md.Z}"

// You can also implement it via an object expression
let md' = { new MyDim }
printfn $"DIM from C# but via Object Expression: %d{md'.Z}"

Cela vous permet de tirer parti en toute sécurité du code C# et des composants .NET écrits en C# moderne lorsqu’ils s’attendent à ce que les utilisateurs puissent consommer une implémentation par défaut.

Cette fonctionnalité implémente le document F# RFC FS-1074.

Interopérabilité simplifiée avec des types de valeur Nullable

Les types (de valeur) Nullable (appelés types Nullable historiquement) ont depuis longtemps été pris en charge par F#, mais l’interaction avec eux a traditionnellement été compliquée, car il fallait construire un wrapper Nullable ou Nullable<SomeType> chaque fois que vous souhaitiez passer une valeur. À présent, le compilateur convertit implicitement un type valeur en Nullable<ThatValueType> si le type cible correspond. Le code suivant est désormais possible :

#r "nuget: Microsoft.Data.Analysis"

open Microsoft.Data.Analysis

let dateTimes = PrimitiveDataFrameColumn<DateTime>("DateTimes")

// The following line used to fail to compile
dateTimes.Append(DateTime.Parse("2019/01/01"))

// The previous line is now equivalent to this line
dateTimes.Append(Nullable<DateTime>(DateTime.Parse("2019/01/01")))

Cette fonctionnalité implémente le document F# RFC FS-1075.

Préversion : index inverses

F# 5 introduit également une préversion pour autoriser les index inverses. La syntaxe est ^idx. Pour obtenir une valeur d’élément 1 à partir de la fin d’une liste, vous pouvez procéder comme suit :

let xs = [1..10]

// Get element 1 from the end:
xs[^1]

// From the end slices

let lastTwoOldStyle = xs[(xs.Length-2)..]

let lastTwoNewStyle = xs[^1..]

lastTwoOldStyle = lastTwoNewStyle // true

Vous pouvez aussi définir des index inverses pour vos propres types. Pour ce faire, vous devez implémenter la méthode suivante :

GetReverseIndex: dimension: int -> offset: int

Voici un exemple pour le type Span<'T> :

open System

type Span<'T> with
    member sp.GetSlice(startIdx, endIdx) =
        let s = defaultArg startIdx 0
        let e = defaultArg endIdx sp.Length
        sp.Slice(s, e - s)

    member sp.GetReverseIndex(_, offset: int) =
        sp.Length - offset

let printSpan (sp: Span<int>) =
    let arr = sp.ToArray()
    printfn $"{arr}"

let run () =
    let sp = [| 1; 2; 3; 4; 5 |].AsSpan()

    // Pre-# 5.0 slicing on a Span<'T>
    printSpan sp[0..] // [|1; 2; 3; 4; 5|]
    printSpan sp[..3] // [|1; 2; 3|]
    printSpan sp[1..3] // |2; 3|]

    // Same slices, but only using from-the-end index
    printSpan sp[..^0] // [|1; 2; 3; 4; 5|]
    printSpan sp[..^2] // [|1; 2; 3|]
    printSpan sp[^4..^2] // [|2; 3|]

run() // Prints the same thing twice

Cette fonctionnalité implémente le document F# RFC FS-1076.

Préversion : surcharges de mots clés personnalisés dans les expressions de calcul

Les expressions de calcul sont une fonctionnalité puissante pour les auteurs de bibliothèque et de framework. Ils vous permettent d’améliorer considérablement l’expressivité de vos composants en vous permettant de définir des membres connus et de former un DSL pour le domaine dans lequel vous travaillez.

F# 5 ajoute la prise en charge de l’aperçu pour la surcharge des opérations personnalisées dans les expressions de calcul. Cela permet l’écriture et la consommation du code suivant :

open System

type InputKind =
    | Text of placeholder:string option
    | Password of placeholder: string option

type InputOptions =
  { Label: string option
    Kind : InputKind
    Validators : (string -> bool) array }

type InputBuilder() =
    member t.Yield(_) =
      { Label = None
        Kind = Text None
        Validators = [||] }

    [<CustomOperation("text")>]
    member this.Text(io, ?placeholder) =
        { io with Kind = Text placeholder }

    [<CustomOperation("password")>]
    member this.Password(io, ?placeholder) =
        { io with Kind = Password placeholder }

    [<CustomOperation("label")>]
    member this.Label(io, label) =
        { io with Label = Some label }

    [<CustomOperation("with_validators")>]
    member this.Validators(io, [<ParamArray>] validators) =
        { io with Validators = validators }

let input = InputBuilder()

let name =
    input {
    label "Name"
    text
    with_validators
        (String.IsNullOrWhiteSpace >> not)
    }

let email =
    input {
    label "Email"
    text "Your email"
    with_validators
        (String.IsNullOrWhiteSpace >> not)
        (fun s -> s.Contains "@")
    }

let password =
    input {
    label "Password"
    password "Must contains at least 6 characters, one number and one uppercase"
    with_validators
        (String.exists Char.IsUpper)
        (String.exists Char.IsDigit)
        (fun s -> s.Length >= 6)
    }

Avant ce changement, vous pouviez écrire le type InputBuilder tel quel, mais vous ne pouviez pas l’utiliser comme il est utilisé dans l’exemple. Depuis que les surcharges, les paramètres facultatifs et maintenant les types System.ParamArray sont autorisés, tout fonctionne comme prévu.

Cette fonctionnalité implémente le document F# RFC FS-1056.