
我们很高兴宣布Visual Studio 2019将在发布F#的新版本时发布:F#4.6!
F#4.6是对F#语言的较小更新,使其成为“真正的”点发布。 与F#的早期版本一样,F#4.6是完全通过开放的RFC(注释请求)过程开发的。 F#社区在有关此语言版本的讨论中提供了非常详细的反馈。 您可以在此处查看与此版本对应的所有RFC:
这篇文章将详细介绍功能集以及入门方法。
原始博客开始吧
首先,安装:
接下来,将FSharp.Core依赖项更新为FSharp.Core 4.6(或更高版本)。 如果您使用的是Visual Studio,则可以通过NuGet程序包管理UI进行此操作。 如果您不使用Visual Studio,或者希望手动编辑项目文件,请将其添加到项目文件中:
<ItemGroup> <PackageReference Update="FSharp.Core" Version="4.6.0" /> </ItemGroup>
一旦安装了必要的位,就可以将F#4.6与Visual Studio , Visual Studio for Mac或Visual Studio Code与Ionide一起使用 。
匿名记录
除了各种错误修复之外,F#4.6中唯一的语言更改是引入了匿名记录类型 。
基本用法
从仅F#的角度来看,匿名记录是没有显式名称的F#记录类型,可以在临时方式中声明。 尽管它们不太可能从根本上改变您编写F#代码的方式,但是它们确实填补了F#程序员随着时间的推移遇到的许多较小空白,并且可以用于以前无法实现的简洁数据操作。
它们非常易于使用。 例如,在这里您如何与产生匿名记录的函数进行交互:
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
但是,它们不仅仅可以用于基本数据容器。 以下内容扩展了先前的示例,以使用更加类型安全的打印功能:
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
如果您尝试使用具有相同基础数据类型但标签不同的匿名记录来调用`printCircleStats`,则它将无法编译:
printCircleStats r {| Diameter=2.0; Area=4.0; MyCircumference=12.566371 |}
这就是F#记录类型的工作方式,只是所有内容都是临时声明而不是预先声明。 这有其优缺点,取决于您的特定情况,因此我们建议您谨慎使用匿名记录,而不要替换所有的前端F#记录声明。
结构匿名记录
匿名记录也可以通过使用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
您可以调用一个接受结构匿名记录的函数,如下所示:
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 |}
或者,您可以使用“结构推断”在调用站点上消除“ struct”:
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 |}
这会将您创建的匿名记录的实例视为结构。
请注意,相反的说法不正确:
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
当前无法定义IsByRefLike或IsReadOnly结构匿名记录类型。 有一种语言建议可提出这种增强功能,但由于语法方面的奇怪之处,仍在讨论中。
进一步发展
匿名记录可以在更高级的上下文中使用。
匿名记录可序列化
您可以序列化和反序列化匿名记录:
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
这将输出您可能期望的结果:
{"age":28,"name":"Phillip"} Name: Phillip Age: 28
这是在另一个项目中也称为的示例库:
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
对于由微服务组成的系统中的轻量级数据通过网络进行传输的情况,这可能会使事情变得更容易。
匿名记录可以与其他类型定义结合使用
您的域中可能有一个类似树的数据模型,例如以下示例:
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
通常将案例建模为具有命名联合字段的元组,但是随着数据变得越来越复杂,您可以使用记录提取每个案例:
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 }
现在,如果适合您的代码库,则可以使用匿名记录来缩短此递归定义:
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 |}
与前面的示例一样,应明智地应用此技术,并将其应用于您的方案。
匿名记录可简化F#中LINQ的使用
F#程序员通常在处理数据时更喜欢使用List,Array和Sequence组合器,但是使用LINQ有时会有所帮助。 由于LINQ使用C#匿名类型,因此从传统上来说这有点痛苦。
使用匿名记录,可以像使用C#和匿名类型一样使用LINQ方法:
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
打印:
Ana has first letter A Felipe has first letter F Emillia has first letter E
匿名记录可简化与实体框架和其他ORM的合作
使用F#查询表达式与数据库进行交互的F#程序员应该看到匿名记录对生活质量有一些改善。
例如,您可能习惯于使用元组对带有`select`子句的数据进行分组:
let q = query { for row in db.Status do select (row.StatusID, row.Name) }
但这会导致名称不理想的列(如Item1和Item2) 。 在匿名记录之前,您需要声明一个记录类型并使用它。 现在您不需要这样做:
let q = query { for row in db.Status do select {| StatusID = row.StatusID; Name = row.Name |} }
不需要预先指定记录类型! 这使得查询表达式与它们所建模的实际SQL更加一致。
匿名记录还使您避免在更高级的查询中创建AnonymousObject类型,而只是为了查询目的而创建临时数据分组。
匿名记录简化了ASP.NET Core中自定义路由的使用
您可能已经在使用带有F#的ASP.NET Core,但是在定义自定义路由时可能遇到了尴尬。 与前面的示例一样,仍然可以通过预先定义记录类型来完成此操作,但是F#开发人员通常认为这是不必要的。 现在您可以内联:
app.UseMvc(fun routes -> routes.MapRoute("blog","blog/{*article}", defaults={| controller="Blog"; action="Article" |}) |> ignore ) |> ignore
由于F#对于返回类型严格(事实上,与C#不同,在C#中您无需显式忽略返回值的事物),这仍然不是理想的选择。 但是,这确实使您可以删除以前定义的记录定义,这些记录定义除了允许您将数据发送到ASP.NET中间件管道之外,没有其他用途。
使用匿名记录复制和更新表达式
与记录类型一样,您可以对匿名记录使用复制和更新语法:
let data = {| X = 1; Y = 2 |} let expandedData = {| data with Z = 3 |}
原始表达式也可以是记录类型:
type R = { X: int } let data = { X=1 } let data' = {| data with Y = 2 |}
您还可以在引用和结构匿名记录之间来回复制数据:
使用F#中的数据时,复制和更新表达式的使用为匿名记录提供了高度的灵活性。
平等与模式匹配
匿名记录在结构上是平等的,并且具有可比性:
{| a = 1+1 |} = {| a = 2 |}
但是,被比较的类型必须具有相同的“形状”:
尽管可以对匿名记录进行等价和比较,但无法对它们进行模式匹配。 这有两个原因:
- 模式必须说明匿名记录的每个字段,与记录类型不同。 这是因为匿名记录不支持结构子类型化-它们是名义类型。
- 模式匹配表达式中没有其他模式,因为每个不同的模式都意味着不同的匿名记录类型。
- 要求说明匿名记录中的每个字段将使模式比使用“点”符号更为冗长。
而是使用“点”-语法从匿名记录中提取值。 这将始终像使用模式匹配一样冗长,而实际上,由于并非总是从匿名记录中提取每个值,因此冗长程度较低。 这是处理匿名记录属于已区分联合的一部分的先前示例的方法:
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
当前有一个开放的建议 ,允许在有限的上下文中在实际上可以启用匿名记录的情况下对它们进行模式匹配。 如果您有建议的用例,请使用该问题进行讨论!
FSharp.Core新增功能
如果不添加F#核心库,那将不是另一个F#版本!
ValueOption扩展
F#4.5中引入的ValueOption类型现在与该类型附加了一些好处:
- DebuggerDisplay属性可帮助进行调试
- IsNone , IsSome , None , Some , op_Implicit和ToString成员
这使它与Option类型具有“奇偶性”。
此外,现在还有一个ValueOption模块,其中包含与Option模块相同的功能:
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
这应该可以减轻对“ ValueOption”是Option的怪异兄弟的担忧,后者没有获得相同的功能集。
tryExactlyOne用于列表,数组和序列
这个优良的功能是由Grzegorz Dziadkiewicz贡献的。 运作方式如下:
List.tryExactlyOne []
总结
尽管F#4.6中的功能列表并不多,但它们仍然相当深入! 我们鼓励您尝试F#4.6并给我们反馈,以便我们可以在正式发布前进行调整。 与往常一样,感谢F#社区在代码和设计讨论方面的贡献,这些帮助我们继续提高F#语言的水平。
欢呼,祝您黑客愉快!