Pull to refresh
0
Microsoft
Microsoft — мировой лидер в области ПО и ИТ-услуг

Announcing F# 4.6 Preview

Reading time10 min
Views1.6K

We’re excited to announce that Visual Studio 2019 will ship a new version of F# when it releases: F# 4.6!


F# 4.6 is a smaller update to the F# language, making it a “true” point-release. As with previous versions of F#, F# 4.6 was developed entirely via an open RFC (requests for comments) process. The F# community has offered very detailed feedback in discussions for this version of the language. You can view all RFCs that correspond with this release here:



This post will detail the feature set and how to get started.


Original in blog

Get started


First, install either:



Next, update your FSharp.Core dependency to FSharp.Core 4.6 (or higher). If you’re using Visual Studio, you can do this with the NuGet Package Management UI. If you are not using Visual Studio, or prefer hand-editing project files, add this to the project file:


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

Once you have installed the necessary bits, you can use F# 4.6 with Visual Studio, Visual Studio for Mac, or Visual Studio Code with Ionide.


Anonymous Records


Aside from various bug fixes, the only language change in F# 4.6 is the introduction of Anonymous Record types.


Basic usage


From an F#-only perspective, Anonymous Records are F# record types that don’t have explict names and can be declared in an ad-hoc fasion. Although they are unlikely to fundamentally change how you write F# code, they do fill many smaller gaps F# programmers have encountered over time, and can be used for succinct data manipuation that was not previously possible.


They’re quite easy to use. For example, here how you can interact with a function that produces an anonymous record:


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

However, they can be used for more than just basic data containers. The following expands the previous sample to use a more type-safe printing function:


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

If you attempt to call `printCircleStats` with an anonymous record that had the same underlying data types but different labels, it will fail to compile:


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"]'

This is exactly how F# record types work, except everything has been declared ad-hoc rather than up-front. This has benefits and drawbacks depending on your particular situation, so we recommend using anonymous records judiciously rather than replacing all your up-front F# record declarations.


Struct anonymous records


Anonymous records can also be structs by using the struct keyword:


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

You can call a function that takes a struct anonymous record can be done explicitly like this:


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 |}

Or you can use “structness inference” to elide the `struct` at the call site:


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 |}

This will treat the instance of the anonymous record you created as if it were a struct.


Note that the reverse is not true:


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 |}

It is not currently possible to define IsByRefLike or IsReadOnly struct anonymous record types. There is a language suggestion that proposes this enhancement, but due to oddities in syntax it is still under discussion.


Taking things further


Anonymous records can be used in a broader set of more advanced contexts.


Anonymous records are serializable


You can serialize and deserialize anonymous records:


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

This outputs what you might expect:


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

Here’s a sample library that is also called in another project:


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

This may make things easier for scenarios like lightweight data going over a network in a system made up of microservices.


Anonymous records can be combined with other type definitions


You may have a tree-like data model in your domain, such as the following example:


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

It is typical to see cases modeled as tuples with named union fields, but as data gets more complicated, you may extract each case with records:


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 }

This recursive definition can now be shortened with anonymous records if it suits your codebase:


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 |}

As with previous examples, this technique should be applied judiciously and when applicable to your scenario.


Anonymous records ease the use of LINQ in F#


F# programmers typically prefer using the List, Array, and Sequence combinators when working with data, but it can sometimes be helpful to use LINQ. This has traditionally been a bit painful, since LINQ makes use of C# anonymous types.


With anonymous records, you can use LINQ methods just as you would with C# and anonymous types:


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

This prints:


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

Anonymous records ease working with Entity Framework and other ORMs


F# programmers using F# query expressions to interact with a database should see some minor quality of life improvements with anonymous records.


For example, you may be used to using tuples to group data with a `select` clause:


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

But this results in columns with names like Item1 and Item2 that are not ideal. Prior to anonymous records, you would need to declare a record type and use that. Now you don’t need to do that:


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

Not need to specify the record type up front! This makes query expressions much more aligned with the actual SQL that they model.


Anonymous records also let you avoid having to create AnonymousObject types in more advanced queries just to create an ad-hoc grouping of data for the purposes of the query.


Anonymous records ease the use of custom routing in ASP.NET Core


You may be using ASP.NET Core with F# already, but may have run into an awkwardness when defining custom routes. As with previous examples, this could still be done by defining a record type up front, but this has often been seen as unnecessary by F# developers. Now you can do it inline:


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

It’s still not ideal due to the fact that F# is strict about return types (unlike C#, where you need not explicitly ignore things that return a value). However, this does let you remove previously-defined record definitions that served no purpose other than to allow you to send data into the ASP.NET middleware pipeline.


Copy and update expressions with anonymous records


As with Record types, you can use copy-and-update syntax with anonymous records:


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 |}

The original expression can also be a record type:


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

You can also copy data to and from reference and struct anonymous records:


// 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 |}

The use of copy-and-update expressions gives anonymous records a high degree of flexibility when working with data in F#.


Equality and pattern matching


Anonymous records are structurally equatable and comparable:


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

However, the types being compared must have the same “shape”:


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

Although you can equate and compare anonymous records, you cannot pattern match over them. This is for two reasons:


  • A pattern must account for every field of an anonymous record, unlike record types. This is because anonymous records do not support structural subtyping – they are nominal types.
  • There is no ability to have additional patterns in a pattern match expression, as each distinct pattern would imply a different anonymous record type.
  • The requirement to account for every field in an anonymous record would make a pattern more verbose than the use of “dot” notation.

Instead, “dot”-syntax is used to extract values from an anonymous record. This will always be at most as verbose as if pattern matching were used, and in practice is likely to be less verbose due to not always extracting every value from an anonymous record. Here’s how to work with a previous example where anonymous records are a part of a discriminated union:


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

There is currently an open suggestion to allow pattern matching on anonymous records in the limited contexts that they could actually be enabled. If you have a proposed use case, please use that issue to discuss it!


FSharp.Core additions


It wouldn’t be another F# release without additions to the F# Core Library!


ValueOption expansion


The ValueOption type introduces in F# 4.5 now has a few more goodies attached to the type:


  • The DebuggerDisplay attribute to help with debugging
  • IsNone, IsSome, None, Some, op_Implicit, and ToString members

This gives it “parity” with the Option type.


Additionally, there is now a ValueOption module containing the same functions the the `Option` module has:


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

This should alleviate any concerns that `ValueOption` is the weird sibling of `Option` that doesn’t get the same set of functionality.


tryExactlyOne for List, Array, and Seq


This fine function was contributed by Grzegorz Dziadkiewicz. Here’s how it works:


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

Wrapping up


Although the total list of features in F# 4.6 isn’t huge, they still go quite deep! We encourage you to try out F# 4.6 and leave us feedback so that we can fine-tune things before the full release. As always, thank you to the F# community for their contributions – both in code and design discussion – that help us continue to advance the F# language.


Cheers, and happy hacking!

Tags:
Hubs:
+12
Comments0

Articles

Information

Website
www.microsoft.com
Registered
Founded
Employees
Unknown
Location
США