
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 blogCommencez
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 |}
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
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
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 |}
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 |}
Vous pouvez également copier des données vers et depuis des enregistrements anonymes de référence et de structure:
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 |}
Cependant, les types comparés doivent avoir la même «forme»:
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 []
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!