
Wir freuen uns, Ihnen mitteilen zu können, dass Visual Studio 2019 eine neue Version von F # liefert, wenn es veröffentlicht wird: F # 4.6!
F # 4.6 ist ein kleineres Update der F # -Sprache, was es zu einer „echten“ Punktveröffentlichung macht. Wie bei früheren Versionen von F # wurde F # 4.6 vollständig über einen offenen RFC-Prozess (Requests for Comments) entwickelt. Die F # Community hat in Diskussionen zu dieser Version der Sprache sehr detailliertes Feedback gegeben. Sie können alle RFCs, die dieser Version entsprechen, hier anzeigen:
In diesem Beitrag werden die Funktionen und die ersten Schritte beschrieben.
Original im BlogFangen Sie an
Installieren Sie zunächst entweder:
Aktualisieren Sie als Nächstes Ihre FSharp.Core-Abhängigkeit auf FSharp.Core 4.6 (oder höher). Wenn Sie Visual Studio verwenden, können Sie dies mit der NuGet Package Management-Benutzeroberfläche tun. Wenn Sie Visual Studio nicht verwenden oder Projektdateien von Hand bearbeiten möchten, fügen Sie dies der Projektdatei hinzu:
<ItemGroup> <PackageReference Update="FSharp.Core" Version="4.6.0" /> </ItemGroup>
Sobald Sie die erforderlichen Bits installiert haben, können Sie F # 4.6 mit Visual Studio , Visual Studio für Mac oder Visual Studio Code mit Ionide verwenden .
Anonyme Aufzeichnungen
Abgesehen von verschiedenen Fehlerkorrekturen ist die einzige Sprachänderung in F # 4.6 die Einführung anonymer Datensatztypen .
Grundlegende Verwendung
Nur aus F # -Perspektive sind anonyme Datensätze F # -Datensatztypen, die keine expliziten Namen haben und in einer Ad-hoc-Fasion deklariert werden können. Obwohl es unwahrscheinlich ist, dass sie die Art und Weise, wie Sie F # -Code schreiben, grundlegend ändern, füllen sie viele kleinere Lücken, auf die F # -Programmierer im Laufe der Zeit gestoßen sind, und können für prägnante Datenmanipulationen verwendet werden, die zuvor nicht möglich waren.
Sie sind recht einfach zu bedienen. Hier erfahren Sie beispielsweise, wie Sie mit einer Funktion interagieren können, die einen anonymen Datensatz erstellt:
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
Sie können jedoch nicht nur für grundlegende Datencontainer verwendet werden. Im Folgenden wird das vorherige Beispiel um eine typsicherere Druckfunktion erweitert:
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
Wenn Sie versuchen, "printCircleStats" mit einem anonymen Datensatz aufzurufen, der dieselben zugrunde liegenden Datentypen, aber unterschiedliche Bezeichnungen aufweist, kann Folgendes nicht kompiliert werden:
printCircleStats r {| Diameter=2.0; Area=4.0; MyCircumference=12.566371 |}
Genau so funktionieren F # -Datensatztypen, außer dass alles ad-hoc und nicht im Voraus deklariert wurde. Dies hat je nach Ihrer speziellen Situation Vor- und Nachteile. Wir empfehlen daher, anonyme Datensätze mit Bedacht zu verwenden, anstatt alle Ihre vorherigen F # -Datensatzdeklarationen zu ersetzen.
Strukturieren Sie anonyme Datensätze
Anonyme Datensätze können auch Strukturen sein, indem das Schlüsselwort struct verwendet wird :
open System let circleStats radius = let d = radius * 2.0 let a = Math.PI * (radius ** 2.0) let c = 2.0 * Math.PI * radius
Sie können eine Funktion aufrufen, die einen anonymen Strukturdatensatz verwendet. Dies kann explizit folgendermaßen erfolgen:
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 |}
Oder Sie können "Strukturinferenz" verwenden, um die "Struktur" an der Aufrufstelle zu entfernen:
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 |}
Dadurch wird die Instanz des anonymen Datensatzes, den Sie erstellt haben, so behandelt, als wäre es eine Struktur.
Beachten Sie, dass das Gegenteil nicht der Fall ist:
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
Es ist derzeit nicht möglich, anonyme Datensatztypen für IsByRefLike oder IsReadOnly zu definieren. Es gibt einen Sprachvorschlag , der diese Verbesserung vorschlägt, der jedoch aufgrund von Kuriositäten in der Syntax noch diskutiert wird.
Die Dinge weiter bringen
Anonyme Datensätze können in einem breiteren Satz fortgeschrittener Kontexte verwendet werden.
Anonyme Datensätze sind serialisierbar
Sie können anonyme Datensätze serialisieren und deserialisieren:
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
Dies gibt aus, was Sie erwarten könnten:
{"age":28,"name":"Phillip"} Name: Phillip Age: 28
Hier ist eine Beispielbibliothek, die auch in einem anderen Projekt aufgerufen wird:
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
Dies kann Szenarien wie Lightweight-Daten, die über ein Netzwerk in einem System aus Microservices übertragen werden, vereinfachen.
Anonyme Datensätze können mit anderen Typdefinitionen kombiniert werden
Möglicherweise befindet sich in Ihrer Domäne ein baumartiges Datenmodell, z. B. das folgende Beispiel:
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
Es ist typisch, Fälle als Tupel mit benannten Vereinigungsfeldern zu modellieren. Wenn die Daten jedoch komplizierter werden, können Sie jeden Fall mit Datensätzen extrahieren:
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 }
Diese rekursive Definition kann jetzt mit anonymen Datensätzen gekürzt werden, wenn sie zu Ihrer Codebasis passt:
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 |}
Wie bei den vorherigen Beispielen sollte diese Technik mit Bedacht und gegebenenfalls auf Ihr Szenario angewendet werden.
Anonyme Datensätze vereinfachen die Verwendung von LINQ in F #
F # -Programmierer bevorzugen normalerweise die Verwendung der Kombinatoren List, Array und Sequence, wenn sie mit Daten arbeiten. Manchmal kann es jedoch hilfreich sein, LINQ zu verwenden. Dies war traditionell etwas schmerzhaft, da LINQ anonyme C # -Typen verwendet.
Bei anonymen Datensätzen können Sie LINQ-Methoden genauso verwenden wie bei C # und anonymen Typen:
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
Dies druckt:
Ana has first letter A Felipe has first letter F Emillia has first letter E
Anonyme Datensätze erleichtern die Arbeit mit Entity Framework und anderen ORMs
F # -Programmierer, die F # -Abfrageausdrücke verwenden, um mit einer Datenbank zu interagieren, sollten bei anonymen Datensätzen geringfügige Verbesserungen der Lebensqualität feststellen.
Beispielsweise können Sie es gewohnt sein, Tupel zu verwenden, um Daten mit einer `select` -Klausel zu gruppieren:
let q = query { for row in db.Status do select (row.StatusID, row.Name) }
Dies führt jedoch zu Spalten mit Namen wie Item1 und Item2 , die nicht ideal sind. Vor anonymen Datensätzen müssten Sie einen Datensatztyp deklarieren und diesen verwenden. Jetzt müssen Sie das nicht mehr tun:
let q = query { for row in db.Status do select {| StatusID = row.StatusID; Name = row.Name |} }
Der Datensatztyp muss nicht im Voraus angegeben werden! Dadurch werden Abfrageausdrücke viel stärker an der tatsächlichen SQL ausgerichtet, die sie modellieren.
Mit anonymen Datensätzen müssen Sie außerdem vermeiden, AnonymousObject- Typen in erweiterten Abfragen erstellen zu müssen, um eine Ad-hoc-Gruppierung von Daten für die Zwecke der Abfrage zu erstellen.
Anonyme Datensätze vereinfachen die Verwendung von benutzerdefiniertem Routing in ASP.NET Core
Möglicherweise verwenden Sie ASP.NET Core bereits mit F #, sind jedoch beim Definieren benutzerdefinierter Routen möglicherweise auf Schwierigkeiten gestoßen. Wie bei den vorherigen Beispielen könnte dies immer noch durch Definieren eines Datensatztyps im Voraus erfolgen, dies wurde jedoch von F # -Entwicklern häufig als unnötig angesehen. Jetzt können Sie es inline tun:
app.UseMvc(fun routes -> routes.MapRoute("blog","blog/{*article}", defaults={| controller="Blog"; action="Article" |}) |> ignore ) |> ignore
Es ist immer noch nicht ideal, da F # bei Rückgabetypen streng ist (im Gegensatz zu C #, wo Sie Dinge, die einen Wert zurückgeben, nicht explizit ignorieren müssen). Auf diese Weise können Sie jedoch zuvor definierte Datensatzdefinitionen entfernen, die nur dazu dienten, Daten an die ASP.NET-Middleware-Pipeline zu senden.
Kopieren und aktualisieren Sie Ausdrücke mit anonymen Datensätzen
Wie bei Datensatztypen können Sie die Syntax zum Kopieren und Aktualisieren mit anonymen Datensätzen verwenden:
let data = {| X = 1; Y = 2 |} let expandedData = {| data with Z = 3 |}
Der ursprüngliche Ausdruck kann auch ein Datensatztyp sein:
type R = { X: int } let data = { X=1 } let data' = {| data with Y = 2 |}
Sie können auch Daten in und aus Referenz- und Struktur-anonymen Datensätzen kopieren:
Die Verwendung von Kopier- und Aktualisierungsausdrücken bietet anonymen Datensätzen ein hohes Maß an Flexibilität bei der Arbeit mit Daten in F #.
Gleichheit und Musterübereinstimmung
Anonyme Datensätze sind strukturell gleichwertig und vergleichbar:
{| a = 1+1 |} = {| a = 2 |}
Die verglichenen Typen müssen jedoch dieselbe „Form“ haben:
Obwohl Sie anonyme Datensätze gleichsetzen und vergleichen können, können Sie keine Musterübereinstimmung darüber durchführen. Dies hat zwei Gründe:
- Im Gegensatz zu Datensatztypen muss ein Muster jedes Feld eines anonymen Datensatzes berücksichtigen. Dies liegt daran, dass anonyme Datensätze keine strukturelle Untertypisierung unterstützen - es handelt sich um nominelle Typen.
- Es gibt keine Möglichkeit, zusätzliche Muster in einem Musterübereinstimmungsausdruck zu haben, da jedes einzelne Muster einen anderen anonymen Datensatztyp implizieren würde.
- Die Anforderung, jedes Feld in einem anonymen Datensatz zu berücksichtigen, würde ein Muster ausführlicher machen als die Verwendung der Punktnotation.
Stattdessen wird "dot" -Syntax verwendet, um Werte aus einem anonymen Datensatz zu extrahieren. Dies ist immer höchstens so ausführlich, als ob ein Mustervergleich verwendet würde, und in der Praxis ist es wahrscheinlich weniger ausführlich, da nicht immer jeder Wert aus einem anonymen Datensatz extrahiert wird. So arbeiten Sie mit einem vorherigen Beispiel, in dem anonyme Datensätze Teil einer diskriminierten Vereinigung sind:
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
Derzeit gibt es einen offenen Vorschlag , den Mustervergleich für anonyme Datensätze in den begrenzten Kontexten zuzulassen, in denen sie tatsächlich aktiviert werden könnten. Wenn Sie einen vorgeschlagenen Anwendungsfall haben, verwenden Sie dieses Problem, um ihn zu diskutieren!
FSharp.Core Ergänzungen
Es wäre keine weitere F # -Veröffentlichung ohne Ergänzungen der F # -Kernbibliothek!
ValueOption-Erweiterung
Der in F # 4.5 eingeführte ValueOption- Typ enthält jetzt einige weitere Extras:
- Das DebuggerDisplay- Attribut zur Unterstützung beim Debuggen
- IsNone- , IsSome- , None- , Some- , op_Implicit- und ToString- Mitglieder
Dies gibt ihm "Parität" mit dem Optionstyp.
Zusätzlich gibt es jetzt ein ValueOption- Modul, das dieselben Funktionen enthält wie das Option-Modul:
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
Dies sollte alle Bedenken zerstreuen, dass "ValueOption" das seltsame Geschwister von "Option" ist, das nicht die gleiche Funktionalität erhält.
tryExactlyOne für List, Array und Seq
Diese feine Funktion wurde von Grzegorz Dziadkiewicz beigesteuert . So funktioniert es:
List.tryExactlyOne []
Einpacken
Obwohl die Gesamtliste der Funktionen in F # 4.6 nicht riesig ist, gehen sie dennoch ziemlich tief! Wir empfehlen Ihnen, F # 4.6 auszuprobieren und uns Feedback zu geben, damit wir die Dinge vor der vollständigen Veröffentlichung optimieren können. Wie immer danke ich der F # Community für ihre Beiträge - sowohl in der Code- als auch in der Designdiskussion - die uns helfen, die F # -Sprache weiter voranzutreiben.
Prost und viel Spaß beim Hacken!