一、写在前面的话
学习 F# 一定要去体会函数式编程的特点,推荐一下阮一峰的日志《函数式编程入门教程》。
二、在这篇文章中
- 递归函数
- 记录和可区分联合类型
- 模式匹配
- 可选类型
- 度量单位
- 类和接口
- 选用哪种类型
三、原文链接
备注,原文较长,本人分为《F# 之旅》上下两部分翻译。友情链接《F#之旅(上)》。
四、开始
1. 递归函数
在 F# 中,通常使用递归来处理集合和元素序列。虽然 F# 支持循环和命令式编程,但递归函数是首选,因为它更容易保证正确性:
module RecursiveFunctions = /// 此示例展示了一个计算整数阶乘的递归函数。 /// 使用“let rec”定义一个递归函数。 let rec factorial n = if n = 0 then 1 else n * factorial (n-1) printfn "Factorial of 6 is: %d" (factorial 6) /// 计算两个整数的最大公因数。Computes the greatest common factor of two integers. /// /// 由于所有递归调用都是尾部调用, 所以编译器将函数变为循环, 从而提高性能并减少内存占用。 let rec greatestCommonFactor a b = if a = 0 then b elif a < b then greatestCommonFactor a (b - a) else greatestCommonFactor (a - b) b printfn "The Greatest Common Factor of 300 and 620 is %d" (greatestCommonFactor 300 620) /// 这个示例使用递归计算列表中整数的和。 let rec sumList xs = match xs with | [] -> 0 | y::ys -> y + sumList ys /// 这使得'sumList'的尾递归,使用一个帮助函数和结果累加器。 let rec private sumListTailRecHelper accumulator xs = match xs with | [] -> accumulator | y::ys -> sumListTailRecHelper (accumulator+y) ys /// 将“0”作为累加器的初始值调用尾递归函数。 /// 这样的方式在 F# 中很常见的。 let sumListTailRecursive xs = sumListTailRecHelper 0 xs let oneThroughTen = [1; 2; 3; 4; 5; 6; 7; 8; 9; 10] printfn "The sum 1-10 is %d" (sumListTailRecursive oneThroughTen)
F# 还对尾部调用的优化提供了完整支持,这是优化递归调用的一种方法,这样它们就如同循环构造一样快。
2. 记录和可区分联合类型
记录和联合类型 F# 中的两个基本数据类型,而且通常是在 F# 程序中表示数据的最佳方式。尽管在其他语言中,它们与类很像相似,但它们的主要区别之一是它们具有结构相等性语义。这意味着它们之间的比较是直截了当的——仅仅检查一个是否等于另一个。
记录是已命名值的集合,具有可选成员(例如方法)。如果您熟悉C#或Java,那么你应该能感觉到这些与POCO或POJO类似,只是它们是结构平等的且需要较少的代码去定义。
module RecordTypes = /// 该示例展示如何定义一个新的记录类型。 type ContactCard = { Name : string Phone : string Verified : bool } /// 这个示例展示如何实例化一个记录类型。 let contact1 = { Name = "Alf" Phone = "(206) 555-0157" Verified = false } /// 你同样可以使用“;”分割位于同一行的字段。 let contactOnSameLine = { Name = "Alf"; Phone = "(206) 555-0157"; Verified = false } /// 这个示例展示怎样对记录类型进行拷贝和更新。 /// 从contact1拷贝出一个新的记录值,但是具有不同的“Phone”和“Verified”字段。 let contact2 = { contact1 with Phone = "(206) 555-0112" Verified = true } /// 这个示例展示如何向一个函数传递一个记录值。 /// 它转换一个“ContactCard”对象为一个字符串。 let showContactCard (c: ContactCard) = c.Name + " Phone: " + c.Phone + (if not c.Verified then " (unverified)" else "") printfn "Alf's Contact Card: %s" (showContactCard contact1) /// 这是一个拥有成员的记录。 type ContactCardAlternate = { Name : string Phone : string Address : string Verified : bool } /// 成员可以实现面向对象成员。 member this.PrintedContactCard = this.Name + " Phone: " + this.Phone + (if not this.Verified then " (unverified)" else "") + this.Address let contactAlternate = { Name = "Alf" Phone = "(206) 555-0157" Verified = false Address = "111 Alf Street" } // 实例使用“.”操作符访问成员。 printfn "Alf's alternate contact card is %s" contactAlternate.PrintedContactCard
在 F# 4.1,你同样可以将记录表现为结构体。为此使用 [<Struct>] 特性:
/// 记录同样可以使用“Struct”表现为结构体。 /// 这在权衡了结构体的性能胜过引用类型的灵活性时,是非常有用的。 [<Struct>] type ContactCardStruct = { Name : string Phone : string Verified : bool }
可区分联合类型(DU)可以是多个命名形式或事例的值。存储在该类型中的数据可以是几个截然不同的值种的一个。
module DiscriminatedUnions = /// 下面代表纸牌的花色。 type Suit = | Hearts | Clubs | Diamonds | Spades /// 一个可区分联合同样可以代表纸牌的牌位。 type Rank = /// 代表牌位 2 .. 10。 | Value of int | Ace | King | Queen | Jack /// 可区分联合同样可以实现面向对象成员。 static member GetAllRanks() = [ yield Ace for i in 2 .. 10 do yield Value i yield Jack yield Queen yield King ] /// 这是由花色和牌位组合而成的记录类型。 /// 使用记录和可区分联合来表达数据是很正常的。 type Card = { Suit: Suit; Rank: Rank } /// 计算获得包含一副纸牌的列表。 let fullDeck = [ for suit in [ Hearts; Diamonds; Clubs; Spades] do for rank in Rank.GetAllRanks() do yield { Suit=suit; Rank=rank } ] /// 这个示例将“Card”对象转换为字符串。 let showPlayingCard (c: Card) = let rankString = match c.Rank with | Ace -> "Ace" | King -> "King" | Queen -> "Queen" | Jack -> "Jack" | Value n -> string n let suitString = match c.Suit with | Clubs -> "clubs" | Diamonds -> "diamonds" | Spades -> "spades" | Hearts -> "hearts" rankString + " of " + suitString /// 这个例子输出一套纸牌中的所有纸牌。 let printAllCards() = for card in fullDeck do printfn "%s" (showPlayingCard card)
您还将 DU 作为单例可区分的联合, 以帮助对基元类型进行领域建模。通常时间、字符串和其他基元类型用于表示某种事物,因此给出了特定的含义。但是,只使用数据的原始表示会导致赋予错误的值!将每种类型的信息表示为不同的单例联合可以在这种情况下强制确保正确性。
// 单例 DU 通常用于领域建模。它可以安全的接受您基于基元类型定义的扩展类型。// // 单例 DU 不能隐式地从它们封装的类型转换得到。 // 例如,接收 Address 的函数不能接受 string 作为输入,相反亦然。 type Address = Address of string type Name = Name of string type SSN = SSN of int // 如下所示,您可以很轻松地实例化一个单例 DU。 let address = Address "111 Alf Way" let name = Name "Alf" let ssn = SSN 1234567890 /// 当您需要该值时,您可以定义简单的函数解开被封装的值。 let unwrapAddress (Address a) = a let unwrapName (Name n) = n let unwrapSSN (SSN s) = s // 使用上述的函数很简单地就能输出单例 DU。 printfn "Address: %s, Name: %s, and SSN: %d" (address |> unwrapAddress) (name |> unwrapName) (ssn |> unwrapSSN)
在上面的示例中,为了得到单例可区分联合所封装的值,你必需显示地对他进行解封装。
除此之外,DU还支持递归定义,允许您轻松地表示树和固有的递归数据。 例如,以下使用 exists 和 insert 函数表示二叉搜索树。
/// 可区分联合同样支持递归定义。 /// /// 这个示例表示一个二叉搜索树,第一种情况下为空树,另一种情况下由两个子树。 type BST<'T> = | Empty | Node of value:'T * left: BST<'T> * right: BST<'T> /// 搜索二叉树中是否存在某一项。 /// 使用模式匹配进行递归搜索。如果存在则返回 true,否则返回 false。 let rec exists item bst = match bst with | Empty -> false | Node (x, left, right) -> if item = x then true elif item < x then (exists item left) // 检查左子树。 else (exists item right) // 检查右子树。 /// 在二叉搜索树中插入一项。 /// 使用模式匹配递归寻找插入的位置,然后插入新的节点。 /// 如果该项已经存在,则不做任何事情。 let rec insert item bst = match bst with | Empty -> Node(item, Empty, Empty) | Node(x, left, right) as node -> if item = x then node // No need to insert, it already exists; return the node. elif item < x then Node(x, insert item left, right) // Call into left subtree. else Node(x, left, insert item right) // Call into right subtree.
因为 DU 允许您在数据类型中表示树的递归结构,因此操纵递归结构直接的,并保证正确性。 模式匹配也支持,如下所示。
另外,您可以使用 [<Struct>] 特性将 DU 表示为结构体:
/// 可区分联合同样也可以使用“Struct”特性表示为结构体。 /// 这在权衡过结构体性能超过引用类型灵活的情况下是有帮助的。 /// /// 但是,当我们这样做的时候,必需要知道这两件很重要的事情: /// 1. 结构体 DU 不能使用递归定义。 /// 2. 一个结构体 DU 必须为每种情况下都有唯一的名字。 [<Struct>] type Shape = | Circle of radius: float | Square of side: float | Triangle of height: float * float
但是,当我们这样做的时候,必需要知道这两件很重要的事情:
1. 结构体 DU 不能使用递归定义。
2. 一个结构体 DU 必须为每种情况下都有唯一的名字。
不遵循上述将导致编译错误。
3. 模式匹配
模式匹配是 F# 语言的特性,它确保在对 F# 类型进行操作的正确性。在上述示例中,你可能注意到了相当多的 match x with ... 语法。这种构造使可以理解数据类型“形状”的编译器,强制您在使用数据类型时考虑所有可能的情况,就是所谓的穷尽模式匹配。这对真确性来说,难以置信的强大,可以巧妙地“转移”通常在编译阶段由运行时关心的事情。
module PatternMatching = /// 一个人姓和名的记录。 type Person = { First : string Last : string } /// 3种不同员工。 type Employee = | Engineer of engineer: Person | Manager of manager: Person * reports: List<Employee> | Executive of executive: Person * reports: List<Employee> * assistant: Employee /// 计算管理层级中的员工下面的所有人员,包括员工。 let rec countReports(emp : Employee) = 1 + match emp with | Engineer(id) -> 0 | Manager(id, reports) -> reports |> List.sumBy countReports | Executive(id, reports, assistant) -> (reports |> List.sumBy countReports) + countReports assistant /// 找到所有没有任何报告的且名为“Dave”的经理/高管。 /// 将"函数" 简写为 lambda 表达式。 let rec findDaveWithOpenPosition(emps : List<Employee>) = emps |> List.filter(function | Manager({First = "Dave"}, []) -> true // []匹配所有空列表。 | Executive({First = "Dave"}, [], _) -> true | _ -> false) // “_” 是一个匹配任何东西的通配符模式。
您还可以在模式匹配的构造中使用简写函数,当您编写使用部分应用程序的功能时,这很有用:
open System let private parseHelper f = f >> function | (true, item) -> Some item | (false, _) -> None let parseDateTimeOffset = parseHelper DateTimeOffset.TryParse let result = parseDateTimeOffset "1970-01-01" match result with | Some dto -> printfn "It parsed!" | None -> printfn "It didn't parse!" let parseInt = parseHelper Int32.TryParse let parseDouble = parseHelper Double.TryParse let parseTimeSpan = parseHelper TimeSpan.TryParse
你应该已经注意到了“_”匹配符。这被称为通配符模式,可以理解为“我不在乎这是什么”。 尽管方便,但如果您不小心的使用“_”,您可能会意外地略过详尽的模式匹配,则不再从编译执行时获益。当模式匹配时,它被适用于如下情况,当您不关心分解类型的某些部分时,或者在模式匹配表达式中枚举出了所有有意义的情况时的最终子句。
活动模式是另一个构造模式匹配的强大方式。它们允许您将输入数据分成自定义形式,并在模式匹配调用处分解它们。 它们也可以被参数化,从而允许将分区定义为一个功能。 扩展前面的例子来支持Active Patterns看起来像这样:
4. 可选类型
可选类型是一种特殊的可区分联合,是 F# 核心库中非常有用的一部分。
可用类型用于表示下面两种情况之一:一个值,没有任何东西。当一个值
5. 度量单位
F# 类型系统的一个独特功能,是能够通过度量单位为数字文本提供上下文。
度量单位允许您将数值类型与单位 (如米) 相关联,并且单位用函数对执行工作而不是数字文本。这样,编译器就可以验证在特定上下文中传递的数字文本类型是否有意义,从而消除了与此类工作相关的运行时错误。
module UnitsOfMeasure = /// 首先,打开常用单位名称的集合。 open Microsoft.FSharp.Data.UnitSystems.SI.UnitNames /// 定义一个常数。 let sampleValue1 = 1600.0<meter> /// 接着定义一个新的单位类型。 [<Measure>] type mile = /// 换算英里到米的系数。1英里=1609.344米。 static member asMeter = 1609.34<meter/mile> /// Define a unitized constant let sampleValue2 = 500.0<mile> /// 计算米制常量。 let sampleValue3 = sampleValue2 * mile.asMeter
printfn "After a %f race I would walk %f miles which would be %f meters" sampleValue1 sampleValue2 sampleValue3
F#核心库定义了许多SI单元类型和单位转换。
6. 类和接口
F# 同样完全支持 .NET 类、接口、抽象类和继承等等。
类表示 .NET 对象的类型,它可以拥有属性、方法和事件作为他的成员。
module DefiningClasses = /// 一个简单的二维向量类。 /// /// 类的构造函数处于第一行, /// 拥有两个参数:dx和dy,都是“double”类型。 type Vector2D(dx : double, dy : double) = /// 这个内部字段存储向量的长度,当这个 /// 对象被构造时计算而出。 let length = sqrt (dx*dx + dy*dy) // “this”为对象自我标识符的名称。 // 在实例方法中,它必需标识在成员名称前。 member this.DX = dx member this.DY = dy member this.Length = length /// 这个成员是方法。上述成员是属性。 member this.Scale(k) = Vector2D(k * this.DX, k * this.DY) /// 这是展示您如何将 Vector2D 类实例化 。 let vector1 = Vector2D(3.0, 4.0) /// 不修改原始对象,获得一个缩放后的新向量。 let vector2 = vector1.Scale(10.0) printfn "Length of vector1: %f Length of vector2: %f" vector1.Length vector2.Length