您能想象这是周期的十分之一! 尽管叙述以前只关注于纯功能样式,但有时切换到面向对象样式会很方便。 面向对象样式的关键特征之一是能够将函数附加到类上并通过一个点访问该类以获得所需的行为。

在F#中,可以使用称为“类型扩展”的功能来实现。 任何F#类型(不仅是类)都可以具有附加函数。
这是将功能附加到记录类型的示例。
module Person = type T = {First:string; Last:string} with // -, member this.FullName = this.First + " " + this.Last // let create first last = {First=first; Last=last} let person = Person.create "John" "Doe" let fullname = person.FullName
要注意的重点:
with
关键字指示成员列表的开始。member
关键字表示该函数是成员(即方法)- 单词
this
是在其上调用此方法的对象的标签(也称为“自我标识符”)。 这个词是函数名称的前缀,在函数内部您可以使用它来引用当前实例。 对于用作自我标识符的单词没有任何要求,只要单词稳定就足够了。 您可以使用this
, self
, me
或通常用作对自己的引用的任何其他单词。
无需添加成员和类型声明,您以后可以始终将其添加到同一模块中:
module Person = type T = {First:string; Last:string} with // , member this.FullName = this.First + " " + this.Last // let create first last = {First=first; Last=last} // , type T with member this.SortableName = this.Last + ", " + this.First let person = Person.create "John" "Doe" let fullname = person.FullName let sortableName = person.SortableName
这些示例说明了对“本机扩展”的调用。 它们被编译成一个类型,并且无论使用该类型的地方都将可用。 使用反射时也会显示它们。
内部扩展甚至允许您将类型定义拆分为多个文件,只要所有组件都使用相同的名称空间并编译为一个程序集即可。 与C#中的部分类一样,这对于分离生成的代码和手写的代码可能很有用。
可选扩展
一种替代方法是从完全不同的模块中添加其他成员。 它们称为“可选扩展名”。 它们不在类内部编译,并且需要使用其他作用域模块来处理它们(此行为类似于C#的扩展方法)。
例如,让一个Person
类型被定义:
module Person = type T = {First:string; Last:string} with // , member this.FullName = this.First + " " + this.Last // let create first last = {First=first; Last=last} // , type T with member this.SortableName = this.Last + ", " + this.First
下面的示例演示如何在另一个模块中向其添加UppercaseName
扩展:
// module PersonExtensions = type Person.T with member this.UppercaseName = this.FullName.ToUpper()
现在,您可以尝试以下扩展程序:
let person = Person.create "John" "Doe" let uppercaseName = person.UppercaseName
糟糕,出现错误。 发生这种情况是因为PersonExtensions
不在范围内。 与C#中一样,要使用任何扩展名,您需要在范围中输入它们。
一旦执行此操作,它将起作用:
// ! open PersonExtensions let person = Person.create "John" "Doe" let uppercaseName = person.UppercaseName
系统类型扩展
您还可以从.NET库扩展类型。 但是应该记住,在扩展类型时,您需要使用其实际名称,而不是别名。
例如,如果您尝试扩展int
,则将无济于事,因为 int
不是该类型的有效名称:
type int with member this.IsEven = this % 2 = 0
而是使用System.Int32
:
type System.Int32 with member this.IsEven = this % 2 = 0 let i = 20 if i.IsEven then printfn "'%i' is even" i
静态成员
您可以使用以下方法创建静态成员函数:
module Person = type T = {First:string; Last:string} with // , member this.FullName = this.First + " " + this.Last // static member Create first last = {First=first; Last=last} let person = Person.T.Create "John" "Doe" let fullname = person.FullName
您可以为系统类型创建静态成员:
type System.Int32 with static member IsOdd x = x % 2 = 1 type System.Double with static member Pi = 3.141 let result = System.Int32.IsOdd 20 let pi = System.Double.Pi
附加现有功能
一个非常常见的模式是将现有独立功能附加到类型上。 它具有以下优点:
- 在开发期间,您可以声明引用其他独立功能的独立功能。 这将简化开发过程,因为使用一种功能样式比使用面向对象的样式(“点对点”)能更好地进行类型推断。
- 但是某些关键功能可以附加到类型上。 这使用户可以选择使用哪种样式-功能性还是面向对象。
这种解决方案的一个示例是F#库中的函数,该函数可计算列表的长度。 您可以使用List
模块中的独立函数,也可以将其作为实例方法调用。
let list = [1..10] // let len1 = List.length list // - let len2 = list.Length
在下面的示例中,类型最初没有任何成员,然后定义了几个函数,最后将fullName
函数附加到该类型。
module Person = // , type T = {First:string; Last:string} // let create first last = {First=first; Last=last} // let fullName {First=first; Last=last} = first + " " + last // type T with member this.FullName = fullName this let person = Person.create "John" "Doe" let fullname = Person.fullName person // let fullname2 = person.FullName //
fullName
函数具有一个参数person
。 附加成员从自链接接收参数。
使用多个参数添加现有功能
还有另一个不错的功能。 如果先前定义的函数带有多个参数,则将其附加到类型时,不必再次列出所有这些参数。 首先指定this
参数就足够了。
在下面的示例中, hasSameFirstAndLastName
函数具有三个参数。 但是,仅附加一个就足够了!
module Person = // type T = {First:string; Last:string} // let create first last = {First=first; Last=last} // let hasSameFirstAndLastName (person:T) otherFirst otherLast = person.First = otherFirst && person.Last = otherLast // type T with member this.HasSameFirstAndLastName = hasSameFirstAndLastName this let person = Person.create "John" "Doe" let result1 = Person.hasSameFirstAndLastName person "bob" "smith" // let result2 = person.HasSameFirstAndLastName "bob" "smith" //
为什么行得通? 提示:考虑使用咖喱和部分使用!
元组方法
当我们拥有带有多个参数的方法时,您需要做出决定:
- 我们可以使用标准(咖喱)形式,其中参数之间用空格分隔,并且支持部分应用程序。
- 或者我们可以一次以逗号分隔的元组形式传递所有参数。
咖喱形式更实用,而元组形式更面向对象。
元组形式还用于与具有标准.NET库的F#进行交互,因此您应该更详细地考虑这种方法。
我们的测试站点将是具有两种方法的Product
类型,每种方法都是通过上述方法之一实现的。 CurriedTotal
和TupleTotal
具有相同的作用:对于给定的数量和折扣,它们计算产品的总成本。
type Product = {SKU:string; Price: float} with // member this.CurriedTotal qty discount = (this.Price * float qty) - discount // member this.TupleTotal(qty,discount) = (this.Price * float qty) - discount
测试代码:
let product = {SKU="ABC"; Price=2.0} let total1 = product.CurriedTotal 10 1.0 let total2 = product.TupleTotal(10,1.0)
到目前为止并没有太大的区别。
但是我们知道,咖喱版本可以部分应用:
let totalFor10 = product.CurriedTotal 10 let discounts = [1.0..5.0] let totalForDifferentDiscounts = discounts |> List.map totalFor10
另一方面,元组版本具有某些无法处理的功能,即:
以元组形式的参数命名参数
元组方法支持命名参数:
let product = {SKU="ABC"; Price=2.0} let total3 = product.TupleTotal(qty=10,discount=1.0) let total4 = product.TupleTotal(discount=1.0, qty=10)
如您所见,这允许您通过显式指定名称来更改参数的顺序。
注意:如果仅某些参数具有名称,则这些参数应始终在末尾。
参数为元组形式的可选参数
对于具有元组形式的参数的方法,可以使用参数名称前面的问号形式的前缀将参数标记为可选参数。
- 如果设置了参数,则将
Some value
传递给函数 - 否则将不会
一个例子:
type Product = {SKU:string; Price: float} with // member this.TupleTotal2(qty,?discount) = let extPrice = this.Price * float qty match discount with | None -> extPrice | Some discount -> extPrice - discount
和测试:
let product = {SKU="ABC"; Price=2.0} // let total1 = product.TupleTotal2(10) // let total2 = product.TupleTotal2(10,1.0)
显式检查None
和Some
可能很乏味,但是有一个用于处理可选参数的更优雅的解决方案。
有一个defaultArg
函数,它将参数名称作为第一个参数,将默认值作为第二个参数。 如果设置了该参数,则将返回相应的值;否则,将返回默认值。
使用defaulArg
的相同代码:
type Product = {SKU:string; Price: float} with // member this.TupleTotal2(qty,?discount) = let extPrice = this.Price * float qty let discount = defaultArg discount 0.0 extPrice - discount
方法重载
在C#中,您可以创建几个名称相同但签名不同的方法(例如,各种类型的参数和/或其编号)。
在纯函数模型中,这没有意义-函数使用特定类型的参数(域)和特定类型的返回值(范围)。 相同的功能无法与其他域和范围进行交互。
但是,F#支持方法重载,但仅适用于方法(附加在类型上)和仅以元组样式编写的方法。
这是TupleTotal
方法的另一个变体的TupleTotal
!
type Product = {SKU:string; Price: float} with // member this.TupleTotal3(qty) = printfn "using non-discount method" this.Price * float qty // member this.TupleTotal3(qty, discount) = printfn "using discount method" (this.Price * float qty) - discount
通常,F#编译器发誓有两个名称相同的方法,但是在这种情况下,这是可以接受的,因为 它们以元组符号声明,并且它们的签名不同。 (为了清楚说明正在调用哪个方法,我添加了一些小消息进行调试)
用法示例:
let product = {SKU="ABC"; Price=2.0} // let total1 = product.TupleTotal3(10) // let total2 = product.TupleTotal3(10,1.0)
y! 没那么快...使用方法的弊端
来自面向对象的世界,您会很想在任何地方使用方法,因为它很熟悉。 但是您应该小心,因为 它们具有许多严重的缺点:
实际上,通过滥用方法,您可能会错过F#编程中最强大,最有用的方面。
让我们看看我的意思。
方法与类型推断的交互性较差
让我们回到使用Person
的示例,其中在独立的函数和方法中实现了相同的逻辑:
module Person = // type T = {First:string; Last:string} // let create first last = {First=first; Last=last} // let fullName {First=first; Last=last} = first + " " + last // - type T with member this.FullName = fullName this
现在,让我们看一下每种方法对类型推断的工作情况。 假设我想打印一个人的全名,那么我将定义printFullName
函数,该函数将person
作为参数。
使用模块中的独立功能进行编码:
open Person // let printFullName person = printfn "Name is %s" (fullName person) // // val printFullName : Person.T -> unit
它编译没有问题,并且类型推断正确地将参数标识为Person
。
现在通过点尝试版本:
open Person // " " let printFullName2 person = printfn "Name is %s" (person.FullName)
这段代码根本不会编译,因为 类型推断没有足够的信息来确定参数的类型。 任何对象都可以实现.FullName
这对于输出来说是不够的。
是的,我们可以为带有参数类型的函数添加注释,但是因此,自动类型推断的整个要点都丢失了。
方法与高阶函数的配合不佳
在高阶函数中也会出现类似的问题。 例如,有一个人员列表,我们需要获取他们的全名列表。
对于独立函数,解决方案很简单:
open Person let list = [ Person.create "Andy" "Anderson"; Person.create "John" "Johnson"; Person.create "Jack" "Jackson"] // list |> List.map fullName
对于对象方法,您必须在各处创建一个特殊的lambda:
open Person let list = [ Person.create "Andy" "Anderson"; Person.create "John" "Johnson"; Person.create "Jack" "Jackson"] // list |> List.map (fun p -> p.FullName)
但这仍然是一个非常简单的示例。 对象的方法非常易于组合,在管道中不方便使用等。
因此,如果您不熟悉函数式编程,那么我敦促您:如果可以,请不要使用方法,尤其是在学习过程中。 它们将使您无法从函数式编程中获得最大的收益。
其他资源
F#的教程很多,包括那些具有C#或Java经验的人的材料。 当您深入了解F#时,以下链接可能会很有用:
还介绍了其他几种开始学习F#的方法 。
最后,F#社区非常适合初学者。 在Slack上,由F#Software Foundation支持的聊天非常活跃,您可以自由加入初学者室。 我们强烈建议您这样做!
不要忘记访问俄语社区F#的网站 ! 如果您对学习语言有任何疑问,我们将很乐意在聊天室中讨论这些问题:
关于翻译作者
由@kleidemos翻译
在F#开发人员的俄语社区的努力下进行了翻译和编辑更改。 我们也感谢@schvepsss和@shwars为本文准备发表。