Annonce de l'aperçu de F # 4.6

Nous sommes ravis d'annoncer que Visual Studio 2019 livrera une nouvelle version de F # lors de sa sortie: F # 4.6!


F # 4.6 est une mise à jour plus petite du langage F #, ce qui en fait une «vraie» version ponctuelle. Comme pour les versions précédentes de F #, F # 4.6 a été entièrement développé via un processus RFC ouvert (demandes de commentaires). La communauté F # a offert des commentaires très détaillés dans les discussions sur cette version du langage. Vous pouvez voir tous les RFC qui correspondent à cette version ici:



Ce message détaillera l'ensemble des fonctionnalités et comment commencer.


Original dans le blog

Commencez


Installez d'abord:



Ensuite, mettez à jour votre dépendance FSharp.Core vers FSharp.Core 4.6 (ou supérieur). Si vous utilisez Visual Studio, vous pouvez le faire avec l'interface utilisateur de gestion des packages NuGet. Si vous n'utilisez pas Visual Studio ou préférez les fichiers de projet de modification manuelle, ajoutez ceci au fichier de projet:


<ItemGroup> <PackageReference Update="FSharp.Core" Version="4.6.0" /> </ItemGroup> 

Une fois que vous avez installé les bits nécessaires, vous pouvez utiliser F # 4.6 avec Visual Studio , Visual Studio pour Mac ou Visual Studio Code avec Ionide .


Enregistrements anonymes


Mis à part diverses corrections de bogues, le seul changement de langue dans F # 4.6 est l'introduction de types d'enregistrement anonyme .


Utilisation basique


Du point de vue F # uniquement, les enregistrements anonymes sont des types d'enregistrement F # qui n'ont pas de noms explicites et peuvent être déclarés de manière ad hoc. Bien qu'il soit peu probable qu'ils changent fondamentalement la façon dont vous écrivez du code F #, ils comblent de nombreuses lacunes plus petites que les programmeurs F # ont rencontrées au fil du temps et peuvent être utilisés pour une manipulation de données succincte qui n'était pas possible auparavant.


Ils sont assez faciles à utiliser. Par exemple, voici comment vous pouvez interagir avec une fonction qui produit un enregistrement anonyme:


 open System let circleStats radius = let d = radius * 2.0 let a = Math.PI * (radius ** 2.0) let c = 2.0 * Math.PI * radius {| Diameter=d; Area=a; Circumference=c |} let r = 2.0 let stats = circleStats r printfn "Circle with radius: %f has diameter %f, area %f, and circumference %f" r stats.Diameter stats.Area stats.Circumference 

Cependant, ils peuvent être utilisés pour plus que des conteneurs de données de base. L'exemple suivant développe l'exemple précédent pour utiliser une fonction d'impression plus sécurisée:


 let circleStats radius = let d = radius * 2.0 let a = Math.PI * (radius ** 2.0) let c = 2.0 * Math.PI * radius {| Diameter=d; Area=a; Circumference=c |} let printCircleStats r (stats: {| Area: float; Circumference: float; Diameter: float |}) = printfn "Circle with radius: %f has diameter %f, area %f, and circumference %f" r stats.Diameter stats.Area stats.Circumference let r = 2.0 let stats = circleStats r printCircleStats r stats 

Si vous essayez d'appeler `printCircleStats` avec un enregistrement anonyme qui avait les mêmes types de données sous-jacents mais des étiquettes différentes, il ne parviendra pas à compiler:


 printCircleStats r {| Diameter=2.0; Area=4.0; MyCircumference=12.566371 |} // Two anonymous record types have mismatched sets of field names '["Area"; "Circumference"; "Diameter"]' and '["Area"; "Diameter"; "MyCircumference"]' 

C'est exactement comme cela que les types d'enregistrement F # fonctionnent, sauf que tout a été déclaré ad hoc plutôt que d'avance. Cela présente des avantages et des inconvénients en fonction de votre situation particulière.Nous vous recommandons donc d'utiliser judicieusement les enregistrements anonymes plutôt que de remplacer toutes vos déclarations d'enregistrements F # initiales.


Structurer des enregistrements anonymes


Les enregistrements anonymes peuvent également être des structures en utilisant le mot clé struct :


 open System let circleStats radius = let d = radius * 2.0 let a = Math.PI * (radius ** 2.0) let c = 2.0 * Math.PI * radius // Note that the keyword comes before the '{| |}' brace pair struct {| Area=a; Circumference=c; Diameter=d |} // the 'struct' keyword also comes before the '{| |}' brace pair when declaring the parameter type let printCircleStats r (stats: struct {| Area: float; Circumference: float; Diameter: float |}) = printfn "Circle with radius: %f has diameter %f, area %f, and circumference %f" r stats.Diameter stats.Area stats.Circumference let r = 2.0 let stats = circleStats r printfn "Circle with radius: %f has diameter %f, area %f, and circumference %f" r stats.Diameter stats.Area stats.Circumference 

Vous pouvez appeler une fonction qui prend un enregistrement anonyme struct peut être fait explicitement comme ceci:


 let printCircleStats r (stats: struct {| Area: float; Circumference: float; Diameter: float |}) = printfn "Circle with radius: %f has diameter %f, area %f, and circumference %f" r stats.Diameter stats.Area stats.Circumference printCircleStats r struct {| Area=4.0; Circumference=12.6; Diameter=12.6 |} 

Ou vous pouvez utiliser «inférence de structure» pour éluder la «structure» sur le site de l'appel:


 let printCircleStats r (stats: struct {| Area: float; Circumference: float; Diameter: float |}) = printfn "Circle with radius: %f has diameter %f, area %f, and circumference %f" r stats.Diameter stats.Area stats.Circumference printCircleStats r {| Area=4.0; Circumference=12.6; Diameter=12.6 |} 

Cela traitera l'instance de l'enregistrement anonyme que vous avez créé comme s'il s'agissait d'une structure.


Notez que l'inverse n'est pas vrai:


 let printCircleStats r (stats: {| Area: float; Circumference: float; Diameter: float |}) = printfn "Circle with radius: %f has diameter %f, area %f, and circumference %f" r stats.Diameter stats.Area stats.Circumference // This will fail to compile for hopefully obvious reasons! printCircleStats r struct {| Area=4.0; Circumference=12.6; Diameter=12.6 |} 

Il n'est actuellement pas possible de définir les types d'enregistrement anonyme de structure IsByRefLike ou IsReadOnly . Il existe une suggestion de langage qui propose cette amélioration, mais en raison de bizarreries dans la syntaxe, elle est toujours en discussion.


Aller plus loin


Les enregistrements anonymes peuvent être utilisés dans un ensemble plus large de contextes plus avancés.


Les enregistrements anonymes sont sérialisables


Vous pouvez sérialiser et désérialiser des enregistrements anonymes:


 open Newtonsoft.Json let phillip = {| name="Phillip"; age=28 |} let str = JsonConvert.SerializeObject(phillip) printfn "%s" str let phillip' = JsonConvert.DeserializeObject<{|name: string; age: int|}>(str) printfn "Name: %s Age: %d" phillip'.name phillip'.age 

Cela produit ce que vous attendez:


 {"age":28,"name":"Phillip"} Name: Phillip Age: 28 

Voici un exemple de bibliothèque qui est également appelée dans un autre projet:


 namespace AnonyRecdOne open Newtonsoft.Json module AR = let serialize () = let phillip = {| name="Phillip"; age=28 |} JsonConvert.SerializeObject(phillip) 

 open AnonyRecdOne open Newtonsoft.Json [<EntryPoint>] let main _ = let str = AR.serialize () let phillip = JsonConvert.DeserializeObject<{|name: string; age: int|}>(str) printfn "Name: %s Age: %d" phillip.name phillip.age 

Cela peut faciliter les choses pour des scénarios tels que des données légères passant sur un réseau dans un système composé de microservices.


Les enregistrements anonymes peuvent être combinés avec d'autres définitions de type


Vous pouvez avoir un modèle de données arborescent dans votre domaine, tel que l'exemple suivant:


 type FullName = { FirstName: string; LastName: string } type Employee = | Engineer of FullName | Manager of name: FullName * reports: Employee list | Executive of name: FullName * reports: Employee list * assistant: Employee 

Il est typique de voir des cas modélisés sous forme de tuples avec des champs d'union nommés, mais à mesure que les données se compliquent, vous pouvez extraire chaque cas avec des enregistrements:


 type FullName = { FirstName: string; LastName: string } type Employee = | Engineer of FullName | Manager of Manager | Executive of Executive and Manager = { Name: FullName; Reports: Employee list } and Executive = { Name: FullName; Reports: Employee list; Assistant: Employee } 

Cette définition récursive peut désormais être raccourcie avec des enregistrements anonymes si elle convient à votre base de code:


 type FullName = { FirstName: string; LastName: string } type Employee = | Engineer of FullName | Manager of {| Name: FullName; Reports: Employee list |} | Executive of {| Name: FullName; Reports: Employee list; Assistant: Employee |} 

Comme pour les exemples précédents, cette technique doit être appliquée judicieusement et lorsqu'elle s'applique à votre scénario.


Les enregistrements anonymes facilitent l'utilisation de LINQ en F #


Les programmeurs F # préfèrent généralement utiliser les combinateurs List, Array et Sequence lorsqu'ils travaillent avec des données, mais il peut parfois être utile d'utiliser LINQ . Cela a toujours été un peu pénible, car LINQ utilise des types anonymes C #.


Avec les enregistrements anonymes, vous pouvez utiliser les méthodes LINQ comme vous le feriez avec C # et les types anonymes:


 open System.Linq let names = [ "Ana"; "Felipe"; "Emillia"] let nameGrouping = names.Select(fun n -> {| Name=n; FirstLetter=n.[0] |}) for ng in nameGrouping do printfn "%s has first letter %c" ng.Name ng.FirstLetter 

Cela imprime:


 Ana has first letter A Felipe has first letter F Emillia has first letter E 

Les enregistrements anonymes facilitent le travail avec Entity Framework et d'autres ORM


Les programmeurs F # utilisant des expressions de requête F # pour interagir avec une base de données devraient voir quelques améliorations mineures de la qualité de vie avec des enregistrements anonymes.


Par exemple, vous pouvez être habitué à utiliser des tuples pour regrouper des données avec une clause `select`:


 let q = query { for row in db.Status do select (row.StatusID, row.Name) } 

Mais cela se traduit par des colonnes avec des noms comme Item1 et Item2 qui ne sont pas idéaux. Avant les enregistrements anonymes, vous devez déclarer un type d'enregistrement et l'utiliser. Maintenant, vous n'avez pas besoin de faire ça:


 let q = query { for row in db.Status do select {| StatusID = row.StatusID; Name = row.Name |} } 

Pas besoin de spécifier le type d'enregistrement à l'avance! Cela rend les expressions de requête beaucoup plus alignées avec le SQL réel qu'elles modélisent.


Les enregistrements anonymes vous permettent également d'éviter de créer des types AnonymousObject dans des requêtes plus avancées simplement pour créer un regroupement ad-hoc de données aux fins de la requête.


Les enregistrements anonymes facilitent l'utilisation du routage personnalisé dans ASP.NET Core


Vous utilisez peut-être déjà ASP.NET Core avec F #, mais vous avez peut-être rencontré une gêne lors de la définition de routes personnalisées. Comme pour les exemples précédents, cela pourrait toujours être fait en définissant un type d'enregistrement à l'avance, mais cela a souvent été considéré comme inutile par les développeurs F #. Vous pouvez maintenant le faire en ligne:


 app.UseMvc(fun routes -> routes.MapRoute("blog","blog/{*article}", defaults={| controller="Blog"; action="Article" |}) |> ignore ) |> ignore 

Ce n'est toujours pas idéal en raison du fait que F # est strict sur les types de retour (contrairement à C #, où vous n'avez pas besoin d'ignorer explicitement les choses qui retournent une valeur). Cependant, cela vous permet de supprimer des définitions d'enregistrement précédemment définies qui ne servaient à rien d'autre que de vous permettre d'envoyer des données dans le pipeline de middleware ASP.NET.


Copier et mettre à jour des expressions avec des enregistrements anonymes


Comme pour les types d'enregistrement, vous pouvez utiliser la syntaxe de copie et de mise à jour avec des enregistrements anonymes:


 let data = {| X = 1; Y = 2 |} let expandedData = {| data with Z = 3 |} // Gives {| X=1; Y=2; Z=3 |} let stringifiedY = {| expandedData with Y="Hello!" |} // Gives {| X=1; Y="Hello!"; Z=3 |} 

L'expression d'origine peut également être un type d'enregistrement:


 type R = { X: int } let data = { X=1 } let data' = {| data with Y = 2 |} // Gives {| X=1; Y=2 |} 

Vous pouvez également copier des données vers et depuis des enregistrements anonymes de référence et de structure:


 // Copy data from a reference record into a struct anonymous record type R1 = { X: int } let r1 = { X=1 } let data1 = struct {| r1 with Y=1 |} // Copy data from a struct record into a reference anonymous record [<Struct>] type R2 = { X: int } let r2 = { X=1 } let data2 = {| r1 with Y=1 |} 

L'utilisation d'expressions de copie et de mise à jour confère aux enregistrements anonymes une grande flexibilité lors de l'utilisation de données en F #.


Égalité et correspondance de motifs


Les enregistrements anonymes sont structurellement équitables et comparables:


 {| a = 1+1 |} = {| a = 2 |} // true {| a = 1+1 |} > {| a = 1 |} // true 

Cependant, les types comparés doivent avoir la même «forme»:


 // error FS0001: Two anonymous record types have mismatched sets of field names '["a"]' and '["a"; "b"]' {| a = 1+1 |} = {| a = 2; b = 1|} 

Bien que vous puissiez assimiler et comparer des enregistrements anonymes, vous ne pouvez pas faire correspondre les modèles sur eux. Ceci pour deux raisons:


  • Un modèle doit tenir compte de chaque champ d'un enregistrement anonyme, contrairement aux types d'enregistrement. En effet, les enregistrements anonymes ne prennent pas en charge le sous-typage structurel - ce sont des types nominaux.
  • Il n'est pas possible d'avoir des modèles supplémentaires dans une expression de correspondance de modèle, car chaque modèle distinct impliquerait un type d'enregistrement anonyme différent.
  • L'obligation de tenir compte de chaque champ dans un enregistrement anonyme rendrait un modèle plus verbeux que l'utilisation de la notation «point».

Au lieu de cela, la syntaxe «point» est utilisée pour extraire les valeurs d'un enregistrement anonyme. Ce sera toujours au plus aussi verbeux que si la correspondance de modèles était utilisée, et dans la pratique, il est probable qu'il soit moins verbeux car il n'extrait pas toujours toutes les valeurs d'un enregistrement anonyme. Voici comment travailler avec un exemple précédent où les enregistrements anonymes font partie d'une union discriminée:


 type Employee = | Engineer of FullName | Manager of {| Name: FullName; Reports: Employee list |} | Executive of {| Name: FullName; Reports: Employee list; Assistant: Employee |} let getFirstName e = match e with | Engineer fullName -> fullName.FirstName | Manager m -> m.Name.FirstName | Executive ex -> ex.Name.FirstName 

Il existe actuellement une suggestion ouverte pour autoriser la correspondance des modèles sur les enregistrements anonymes dans les contextes limités qui pourraient en fait être activés. Si vous avez un cas d'utilisation proposé, veuillez utiliser ce problème pour en discuter!


Ajouts FSharp.Core


Ce ne serait pas une autre version F # sans ajouts à la bibliothèque F # Core!


Extension ValueOption


Le type ValueOption introduit dans F # 4.5 a maintenant quelques autres goodies attachés au type:


  • L'attribut DebuggerDisplay pour aider au débogage
  • Membres IsNone , IsSome , None , Some , op_Implicit et ToString

Cela lui donne une «parité» avec le type d'option.


De plus, il existe maintenant un module ValueOption contenant les mêmes fonctions que le module `Option` a:


 module ValueOption = [<CompiledName("IsSome")>] val inline isSome: voption:'T voption -> bool [<CompiledName("IsNone")>] val inline isNone: voption:'T voption -> bool [<CompiledName("DefaultValue")>] val defaultValue: value:'T -> voption:'T voption -> 'T [<CompiledName("DefaultWith")>] val defaultWith: defThunk:(unit -> 'T) -> voption:'T voption -> 'T [<CompiledName("OrElse")>] val orElse: ifNone:'T voption -> voption:'T voption -> 'T voption [<CompiledName("OrElseWith")>] val orElseWith: ifNoneThunk:(unit -> 'T voption) -> voption:'T voption -> 'T voption [<CompiledName("GetValue")>] val get: voption:'T voption -> 'T [<CompiledName("Count")>] val count: voption:'T voption -> int [<CompiledName("Fold")>] val fold<'T,'State> : folder:('State -> 'T -> 'State) -> state:'State -> voption:'T voption -> 'State [<CompiledName("FoldBack")>] val foldBack<'T,'State> : folder:('T -> 'State -> 'State) -> voption:'T voption -> state:'State -> 'State [<CompiledName("Exists")>] val exists: predicate:('T -> bool) -> voption:'T voption -> bool [<CompiledName("ForAll")>] val forall: predicate:('T -> bool) -> voption:'T voption -> bool [<CompiledName("Contains")>] val inline contains: value:'T -> voption:'T voption -> bool when 'T : equality [<CompiledName("Iterate")>] val iter: action:('T -> unit) -> voption:'T voption -> unit [<CompiledName("Map")>] val map: mapping:('T -> 'U) -> voption:'T voption -> 'U voption [<CompiledName("Map2")>] val map2: mapping:('T1 -> 'T2 -> 'U) -> voption1: 'T1 voption -> voption2: 'T2 voption -> 'U voption [<CompiledName("Map3")>] val map3: mapping:('T1 -> 'T2 -> 'T3 -> 'U) -> 'T1 voption -> 'T2 voption -> 'T3 voption -> 'U voption [<CompiledName("Bind")>] val bind: binder:('T -> 'U voption) -> voption:'T voption -> 'U voption [<CompiledName("Flatten")>] val flatten: voption:'T voption voption -> 'T voption [<CompiledName("Filter")>] val filter: predicate:('T -> bool) -> voption:'T voption -> 'T voption [<CompiledName("ToArray")>] val toArray: voption:'T voption -> 'T[] [<CompiledName("ToList")>] val toList: voption:'T voption -> 'T list [<CompiledName("ToNullable")>] val toNullable: voption:'T voption -> Nullable<'T> [<CompiledName("OfNullable")>] val ofNullable: value:Nullable<'T> -> 'T voption [<CompiledName("OfObj")>] val ofObj: value: 'T -> 'T voption when 'T : null [<CompiledName("ToObj")>] val toObj: value: 'T voption -> 'T when 'T : null 

Cela devrait atténuer les préoccupations selon lesquelles «ValueOption» est le frère étrange de «Option» qui n'obtient pas le même ensemble de fonctionnalités.


tryExactlyOne pour List, Array et Seq


Cette belle fonction a été apportée par Grzegorz Dziadkiewicz . Voici comment cela fonctionne:


 List.tryExactlyOne [] // None List.tryExactlyOne [1] // Some 1 List.tryExactlyOne [1; 2] // None Array.tryExactlyOne null // ArgumentNullException Array.tryExactlyOne [||] // None Array.tryExactlyOne [|1|] // Some 1 Array.tryExactlyOne [|1; 2|] // None Seq.tryExactlyOne null // ArgumentNullException Seq.tryExactlyOne (Seq.ofList []) // None Seq.tryExactlyOne (Seq.ofList [1]) // Some 1 Seq.tryExactlyOne (Seq.ofList [1; 2]) // None 

Envelopper


Bien que la liste totale des fonctionnalités de F # 4.6 ne soit pas énorme, elles vont toujours assez loin! Nous vous encourageons à essayer F # 4.6 et à nous laisser vos commentaires afin que nous puissions affiner les choses avant la sortie complète. Comme toujours, merci à la communauté F # pour ses contributions - à la fois dans la discussion sur le code et la conception - qui nous aident à continuer à faire avancer le langage F #.


Bravo et bon piratage!

Source: https://habr.com/ru/post/fr437666/


All Articles