Let 表达式
让我们看看比传统“Hello, world!”更复杂一些的 F# 代码示例。请看以下代码:
复制代码
let results = [ for i in 0 .. 100 -> (i, i*i) ]
printfn "results = %A" results
在该 F# 语法中,let 表达式是令人产生好奇的元素。它是整个语言中最重要的表达式。更正式地说,let 可以为标识符赋值。用 Visual Basic 和 C# 开发人员的行话来说,“它可以定义一个变量”。但这并不确切。在 F# 中,标识符包含两个要素。首先,标识符一旦定义就不能再更改。(这即是 F# 帮助程序员创建并发安全程序的方法,因为它不提倡可变的状态。)第二,标识符不仅可以是基元类型或对象类型(如 C# 和 Visual Basic 中所使用的类型),而且还可以是函数类型,这一点与 LINQ 相似。
同时还请注意,标识符从不显式定义为类型。例如,从不定义结果标识符;它将从后面表达式的右侧进行推断。这称为类型推断,并且它代表编译器分析代码、确定返回值和自动插入返回值的能力(这与新的 C# 推断类型表达式通过变量关键字进行推断的方法类似)。
Let 表达式不仅可以处理数据,还可以使用它来定义函数,F# 将函数看作第一级概念。下面的示例定义了一个加法函数,它使用两个参数 a 和 b:
复制代码
let add a b =
a + b
完全按照您预期的方式工作:将 a 与 b 相加,并将结果显式返回给调用者。这意味着从技术上讲,F# 中的每个函数都将返回一个值,即使返回的不一定是值,也会返回一个特殊的名称 unit。这将在 F# 代码中产生一些有趣的暗示,特别是与 .NET Framework 类库相交的部分。但目前,C# 和 Visual Basic 开发人员可以把 unit 大致看作是与 void 相同的类型。
有时函数应该忽略传递给它的参数。要在 F# 中达此目的,仅需使用下划线作为该参数的占位符即可:
复制代码
let return10 _ =
add 5 5
// 12 is effectively ignored, and ten is set to the resulting
// value of add 5 5
let ten = return10 12
printf "ten = %d\n" ten
与许多函数式语言类似,F# 允许根据其调用进行 currying(可以仅部分定义函数的应用),以便提供其余的参数:
复制代码
let add5 a =
add a 5
在某种程度上,这与创建一个接受不同的参数集并调用其他方法的重载方法相似:
复制代码
public class Adders {
public static int add(int a, int b) { return a + b; }
public static int add5(int a) { return add(a, 5); }
}
但两者还是有一点细微的差别。请注意,在 F# 版本中,没有显式定义类型。这表示编译器将采用自己的类型推断方法确定 add5 的参数是否是与加上整数 5 兼容的类型,并确定是按照这种方式编译,还是将其标记为错误。事实上,F# 语言主要使用隐式类型参数化(即使用泛型)。
在 Visual Studio 中,将指针停放在前面所显示的 ten 的定义上时,将表明其类型声明为:
val ten : ('a -> int)
在 F# 中,这表示 ten 是一个值,一个获取任意类型的参数并产生整数结果的函数。这种记号语法大致等同于 C# 中的 <T> 语法,所以对 C# 函数最贴切的说法是:ten 是类型参数化方法的委托实例,您想要忽略其类型(但在 C# 规则下不可忽略):
delegate int Transformer<T>(T ignored);
public class App
{
public static int return10(object ignored) { return 5 + 5; }
static void Main()
{
Transformer<object> ten = return10;
System.Console.WriteLine("ten = {0}", return10(0));
}
}
let compute2 x = (x, x*x)
let compute3 x = (x, x*x, x*x*x)
let results2 = [ for i in 0 .. 100 -> compute2 i ]
let results3 = [ for i in 0 .. 100 -> compute3 i ]
遍历列表(或数组或其他一些重复结构)是函数式语言中很常见的任务,它已成为基本方法调用:List.iter。它仅简单地对列表中的每个元素调用一个函数。其他类似的库函数还可提供一些非常有用的功能。例如,List.map 将函数作为参数,并将该函数应用于列表中的每个元素,并返回该过程产生的新列表。
管道
让我们讨论 F# 中另一个结构——管道操作符,它通过类似命令 shell(如 Windows PowerShell?)管道的通道获取函数的结果,并将结果用作后一个函数的输入。我们来看图 2 中显示的 F# 代码段。该代码段使用 System.Net 命名空间连接 HTTP 服务器,获取相应的 HTML 并分析结果。
Figure 2 检索和分析 HTML
复制代码
/// Get the contents of the URL via a web request
let http(url: string) =
let req = System.Net.WebRequest.Create(url)
let resp = req.GetResponse()
let stream = resp.GetResponseStream()
let reader = new System.IO.StreamReader(stream)
let html = reader.ReadToEnd()
resp.Close()
html
let getWords s = String.split [ ' '; '\n'; '\t'; '<'; '>'; '=' ] s
let getStats site =
let url = "http://" + site
let html = http url
let words = html |> getWords
let hrefs = html |> getWords |> List.filter (fun s -> s = "href")
(site,html.Length, words.Length, hrefs.Length)
请注意 getStats 定义中的 words 标识符。它获取从 URL 返回的 html 值,并对其应用 getWords 函数。我还可以编写定义读取:
复制代码
let words = getWords html
两者等同。但是 hrefs 标识符显示了管道操作符的威力,通过管道操作符可以将任意多个应用程序连接起来。此处我获取 words 的结果列表,并将其通过管道传递给 List.filter 函数,该函数使用匿名函数查找单词 href,并在表达式为 true 时将其返回。并且,最重要的是,getStats 调用的结果将是另一个聚合 (string * int *int * int)。要使用 C# 编写,需要的远远不止 15 行代码。
图 2 中的示例还显示出更多 F# 与 .NET Framework 的兼容性,以下代码也表现出这一特性:
复制代码
open System.Collections.Generic
let capitals = Dictionary<string, string>()
capitals.["Great Britain"] <- "London"
capitals.["France"] <- "Paris"
capitals.ContainsKey("France")
确实,这个示例除了练习 Dictionary<K,V> 类型外没有什么其他内容,但它显示出在 F# 中如何指定泛型(使用与 C# 相同的尖括号)、如何在 F# 中使用索引(同样与 C# 一样使用方括号),以及如何执行 .NET 方法(使用与 C# 中同样的点和圆括号)。事实上,这里仅有的新内容是使用左箭头操作符为可变值赋值。这一点是必需的,因为 F# 与大多数函数式语言一样,保留等号用于比较,以便保持数学符号含义:如果 x = y,则 x 与 y 的值相等,而不是将 y 的值赋给 x。(真正的数学家们早已对普遍存在或设想过的 x = x + 1 提出异议,甚至偷笑不已。)
F# 也能够处理对象
当然,并不是所有开始使用 .NET 的开发人员都愿意立即接受函数式的概念。事实上,大多数从 C# 或 Visual Basic 转向 F# 的开发人员都需要知道他们在使用这一新语言时可以保留原有的习惯。在某种程度上,这是完全可行的。
例如,请看图 3 顶部所示的二维向量的类定义。其中就有一些有趣的概念。首先,请注意其中没有显式构造函数体;第一行中的参数表明用于构造 Vector2D 实例的参数本质上就是构造函数。因此长度标识符,以及 dx 和 dy 标识符将成为 Vector2D 类型的私有元素,而 member 关键字则表明可以通过标准 .NET 属性访问获取的 Vector2D 外部可用成员。本质上,这段 F# 代码声明了您可在图 3 底部看到的内容(由 Reflector 报告)。
Figure 3 F# 和 C# 中的矢量变体
复制代码
VECTOR2D IN F#
type Vector2D(dx:float,dy:float) =
let length = sqrt(dx*dx + dy*dy)
member obj.Length = length
member obj.DX = dx
member obj.DY = dy
member obj.Move(dx2,dy2) = Vector2D(dx+dx2,dy+dy2)
VECTOR2D IN C# (REFLECTOR>
[Serializable, CompilationMapping(SourceLevelConstruct.ObjectType)]
public class Vector2D
{
// Fields
internal double _dx@48;
internal double _dy@48;
internal double _length@49;
open System
open System.IO
open System.Windows.Forms
open Printf
let form = new Form(Text="My First F# Form", Visible=true)
let menu = form.Menu <- new MainMenu()
let mnuFile = form.Menu.MenuItems.Add("&File")
let filter = "txt files (*.txt)|*.txt|All files (*.*)|*.*"
let mnuiOpen =
new MenuItem("&Open...",
new EventHandler(fun _ _ ->
let dialog =
new OpenFileDialog(InitialDirectory="c:\\",
Filter=filter;
FilterIndex=2,
RestoreDirectory=true)
if dialog.ShowDialog() = DialogResult.OK then
match dialog.OpenFile() with
| null -> printf "Could not read the file...\n"
| s ->
let r = new StreamReader(s)
printf "First line is: %s!\n" (r.ReadLine());
s.Close();
),
Shortcut.CtrlO)
mnuFile.MenuItems.Add(mnuiOpen)
[<STAThread>]
do Application.Run(form)
任何熟悉 Windows 窗体的开发人员都能够立即明白这些代码的含义:创建一个简单的窗体、填充一些属性、填入一个事件处理程序,并告诉应用程序开始运行,直到用户单击右上角的“关闭”按钮。由于标准元素与 .NET 应用程序相同,所以只需重点关注 F# 语法即可。
Open 语句的操作与 C# 中 using 语句的作用大致相同,本质上都是打开 .NET 命名空间以便在没有正式限制符的情况下使用。Printf 命名空间是 F# 原有的、技术上与 OCaml 模块具有相同名称的端口。F# 不仅具备完整的 .NET Framework 类库,而且还有最简洁的 OCaml 库端口,这使得熟悉该语言的程序员能够象使用 .NET Framework 一样对其运用自如。(致好奇心强的程序员:Printf 位于 FSharp.Core.dll 程序集中。)您完全可以根据个人偏好随时使用 System.Console.WriteLine。
窗体标识符的创建利用了 F# 命名参数,它等同于实例化对象,然后调用一系列属性集来为这些属性填充值。我在下面的几行中使用相同的方法创建对话框标识符。
mnuiOpen 标识符的定义包含令人感兴趣的结构,该结构对于熟悉 .NET Framework 2.0 匿名委托或 .NET Framework 3.5 中 lambda 表达式的开发人员来说并不陌生。构造与 Open MenuItem 关联的 EventHandler 时,您可以看到使用以下语法定义的匿名函数:
复制代码
fun _ _ -> ...
类似于匿名委托,这段代码创建了一个将会在选中菜单项时调用的函数,但语法略有技巧性。
MenuItem 定义中对 EventHandler 的定义是忽略传递给它的两个参数的匿名函数,这两个参数巧妙地对应标准 EventHandler 委托类型中的发送方和事件参数。该函数规定显示新的 OpenFileDialog 并在单击“确定”时检查结果...如下所示:
复制代码
if dialog.ShowDialog() = DialogResult.OK then
match dialog.OpenFile() with
| null -> printf "Could not read the file...\n"
| s ->
let r = new StreamReader(s) in
printf "First line is: %s!\n" (r.ReadLine());
s.Close();
将使用模式匹配检查结果,该方法是函数化语言世界中一项强大的功能。模式匹配表面上与 C# 中的 switch/case 在某些地方存在相似之处,但实际上它名副其实地完成模式匹配工作:它将值与各种不同的模式进行比较(这些模式不需要都是常量值),并执行匹配的代码块。因此,以此处所示的匹配块为例,OpenFile 的结果可以匹配两种可能的值:null 表示无法打开任何文件,或者分配任何非 null 值的 s,该值将随后用作 StreamReader 的构造函数来打开并读取给定文本文件的第一行。
模式匹配是大多数函数式语言的重要部分,对它做些研究是完全值得的。它的一个最常见的用途是与可辨识联合 (discriminated union) 类型(C# 或 Visual Basic 中枚举类型的不确切说法)配合使用:
复制代码
// Declaration of the 'Expr' type
type Expr =
| Binary of string * Expr * Expr
| Variable of string
| Constant of int
// Create a value 'v' representing 'x + 10'
let v = Binary("+", Variable "x", Constant 10)
函数式语言中常用它来创建域特定语言的核心表示,开发人员可以使用它来编写更为复杂和强大的结构。例如,不难想象扩展此语法以创建完全计算式语言,并可简单地通过为 Expr 类型添加新元素而进一步扩展该语言。这里需要注意的是:使用 * 字符的语法并不表示使用乘法,它是函数式语言中用于指示某类型中包含多个部分的标准方式。
事实上,函数式语言已经非常普遍地应用于编写面向语言的编程工具(如解释器和编译器),并且 Expr 类型最终将成为语言表达式类型的完整集合。F# 通过其内置的两个工具:fslex 和 fsyacc(专为获得传统语言输入—lex 和 yacc 文件—并将其编译成 F# 代码以便简化操作而设计)使这一切变得更为简单。如果对此感兴趣,可以下载 F# 安装程序深入研究;特别是标准 F# 发行包中的 Parsing 示例将提供非常好的入门基础结构。
可辨识联合只是模式匹配的优势之一,另一项优势是表达式的执行,如图 5 所示。位于 eval 定义中的 rec 是必需的,它告诉 F# 编译器在定义主体内迭代调用 eval。如果没有它,F# 将期望出现一个名为 eval 的本地嵌套函数。实际使用时,我使用函数 getVarValue 为变量返回一些预定义的值,getVarValue 将检查 Dictionary,查找变量创建时确定的返回值。
Figure 5 表达式执行
复制代码
let getVarValue v =
match v with
| "x" -> 25
| "y" -> 12
| _ -> 0
let rec eval x =
match x with
| Binary(op, l, r) ->
let (lv, rv) = (eval l, eval r) in
if (op = "+") then lv + rv
elif (op = "-") then lv - rv
else failwith "E_UNSUPPORTED"
| Variable(var) ->
getVarValue var
| Constant(n) ->
n
do printf "Results = %d\n" (eval v)
当调用 eval 时,它将得到值 v 并发现该值是一个 Binary 值。这与第一个子表达式匹配,该表达式随后把值 (lv, rv) 绑定到刚检查的 Binary 值左右两侧的计算结果。未命名的值 (lv, rv) 是一个聚合(本质上是代表多个部分的单个值),这一点与关系集或 C 结构相似。
当首次调用 eval l 时,来自 Binary 实例的 l 恰好是 Variable 类型,因此对 eval 的递归调用匹配该模式匹配块的分支。随后将调用 getVarValue,它会返回硬编码 25,该值最终将绑定到值 lv。对于包含值 10 的常量 r 来说顺序相同,因此它将绑定到 rv。然后执行代码块的剩余部分(if/else-if/else 块),熟悉 C#、Visual Basic 或 C++ 的开发人员可以很容易地读懂该代码块的含义。
这里需要再次强调的是:每个表达式都将返回一个值,甚至在模式匹配块内部也一样。在本例中,返回值是一个整型值,该值可能是运算得到的值、从变量中检索到的值或者是常量本身。这一点似乎更容易让习惯于面向对象或过程化编程的开发人员产生微词,因为在 C#、Visual Basic 或 C++ 中,返回值是可选的,并且甚至在指定返回值的情况下仍可以忽略返回值。在类似 F# 的函数式语言中,要忽略返回值需要显式编码表达方式。如果出现这种情况,程序员可以将结果传给名为 ignore 的函数,由它完成适当的操作。
异步 F#
目前为止,我对 F# 语法的介绍采用以下两种方式中的一种:或者使用相对简单的函数式结构,或者使其看起来比较初级且简洁,象是传统面向对象、.NET 兼容语言(C#、Visual Basic 或 C++/CLI)的变体。这种介绍很难推动在企业内采用 F#。
但是请看一下图 6。它可与前面两种形式截然不同。除了多处出现 ! 字符并使用 async 修饰符外,这是一段看起来相对比较直观的代码:加载源图像映像、提取其数据、将数据传递到独立的函数进行加工(旋转、拉伸或其他操作),并将数据写回输出文件。
Figure 6 处理图像
复制代码
let TransformImage pixels i =
// Some kind of graphic manipulation of images
let ProcessImage(i) =
async { use inStream = File.OpenRead(sprintf "source%d.jpg" i)
let! pixels = inStream.ReadAsync(1024*1024)
let pixels' = TransformImage(pixels,i)
use outStream = File.OpenWrite(sprintf "result%d.jpg" i)
do! outStream.WriteAsync(pixels')
do Console.WriteLine "done!" }
let ProcessImages() =
Async.Run (Async.Parallel
[ for i in 1 .. numImages -> ProcessImage(i) ])
较不明显的是使用 async 修饰符使这段代码进入 F# 所称的异步工作流(与 Windows Workflow Foundation 无关)中,这意味着这些加载/处理/保存步骤的每一步都在 .NET 线程池的平行线程中执行。
为了使其更简单,看一下图 7 中的代码。这种特殊的顺序以相对简单且易于理解的方式显示出异步工作流。不用深究细节,我们就可以看出 evals 是一组待执行的函数,通过 Async.Parallel 调用使其中每个函数都在线程池中排队等待执行。当执行时,可以看出实际上 evals 中的函数与 awr 中的函数在不同的线程中(尽管由于 .NET 系统线程池的特性,部分或全部 evals 函数有可能在相同的线程中执行)。
Figure 7 异步执行函数
复制代码
#light
open System.Threading
let evals =
let z = 4.0
[ async { do printWithThread "Computing z*z\n"
return z * z };
async { do printWithThread "Computing sin(z)\n"
return (sin z) };
async { do printWithThread "Computing log(z)\n"
return (log z) } ]
let awr =
async { let! vs = Async.Parallel evals
do printWithThread "Computing v1+v2+v3\n"
return (Array.fold_left (fun a b -> a + b) 0.0 vs) }
1.不可变性(Immutability)
您也许已经注意到,我一直使用“值(value)”来表示一个标识符(identifier),而不是“变量(variable)”。这是由于默认情况下,F#中的类型是不可变的(immutable),也就是说,一经创建即不可修改。看起来这是一个很大的限制,但是不可变性可以避免某种类型的bug。另外,不可变的数据天然地具备线程安全的特性,这意味着您无需在处理并行情况时担心同步锁的发生。我将在系列的第三篇中介绍异步编程。
如果您确实需要修改数据,可使用F#的mutable关键字,它会创建一个变量(而不是值)。我们可以通过左箭头操作符(<-)来修改变量的值。
> let mutable x = "the original value.";;
val mutable x : string
> printfn "x's value is '%s'" x;;
x's value is 'the original value.'
val it : unit = ()
> x <- "the new one.";;
val it : unit = ()
> printfn "x's value is now '%s'" x;;
x's value is now 'the new one.'
val it : unit = ()
2. 引用值(Reference values,Microsoft.FSharp.Core.Ref<_>)
引用值是另一种表示可修改数据的方式。但它不是将变量存储在堆栈(stack),引用值其实是一个指向存储在堆(heap)上的变量的指针(pointer)。在F#中使用可修改的值时会有些限制(比如不可以在内部lambda表达式中使用)。而ref对象则可被安全地传递,因为它们是不可变的record值(只是它有一个可修改的字段)。
使用引用值时,用“:=”赋一个新值,使用“!”进行解引用。
> let refCell = ref 42;;
val refCell : int ref
> refCell := -1;;
val it : unit = ()
> !refCell;;
val it : int = –1
3. 模块(Modules)
在上篇文章中,我只是随意地声明了几个值和函数。您也许会问,“要把它们放在哪里呢?”,因为在C#中所有一切都要属于相应的类。尽管在F#中,我们仍然可以用熟悉的方式声明标准的.NET类,但它也有模块的概念,模块是值、函数和类型的集合(可以对比一下命名空间,后者只能包含类型)。
这也是我们能够访问“List.map”的原因。在F#库(FSharp.Core.dll)中,有一个名为“List”的模块,它包含了函数“map”。
在快速开发的过程中,如果不需要花费时间去设计严格的面向对象类型体系,就可以采用模块来封装代码。要声明自己的模块,要使用module关键字。在下面的例子中,我们将为模块添加一个可修改的变量,该变量也是一个全局变量。
module ProgramSettings =
let version = "1.0.0.0"
let debugMode = ref false
module MyProgram =
do printfn "Version %s" ProgramSettings.version
open ProgramSettings
debugMode := true
4. 元组(Tuples)
元组(tuple,发音为‘two-pull’)表示值的有序集合,而这些值可看作一个整体。按传统的方式,如果您要传递一组相关的值,需要创建结构(struct)或类(class),或者还需要“out”参数。使用元组我们可以将相关的值组织起来,同时并不需要引入新的类型。
要定义一个元组,只要将一组值用逗号分隔,并用圆括号把它们括起来即可。
> let tuple = (1, false, "text");;
val tuple : int * bool * string
> let getNumberInfo (x : int) = (x, x.ToString(), x * x);;
val getNumberInfo : int -> int * string * int
> getNumberInfo 42;;
val it : int * string * int = (42, "42", 1764)
函数甚至可以接受元组为参数:
> let printBlogInfo (owner, title, url) = printfn "%s's blog [%s] is online at '%s'" owner title url;;
val printBlogInfo : string * string * string -> unit
> let myBlog = ("Chris", "Completely Unique View", "http://blogs.msdn.com/chrsmith");;
val myBlog : string * string * string
> printBlogInfo myBlog;;
Chris's blog [Completely Unique View] is online at 'http://blogs.msdn.com/chrsmith'
val it : unit = ()
5. 函数柯里化(Function Currying)
F#提供的一个新奇的特性是可以只接受参数的一个子集,而接受部分参数的结果则是一个新的函数。这就是所谓的“函数柯里化”。比如,假设有一个函数接受3个整数,返回它们的和。我们可以只传入第一个参数,假设值为10,这样我们就可以说将原来的函数柯里化了,而它会返回一个新的函数——新函数接受两个整数,返回它们与10的和。
> let addThree x y z = x + y + z;;
val addThree : int -> int -> int -> int
> let addTwo x y = addThree 10 x y;;
val addTwo : int -> int -> int
> addTwo 1 1;;
val it : int = 12
6. Union类型(Union Types,Discriminated Unions)
考虑下面的枚举值:
enum CardSuit { Spade = 1, Club = 2, Heart = 3, Diamond = 4};
理论上,一个card实例只有一种可能的取值,但由于enum本质上只是整数,您不能确定它的值是否是有效的,在C#中,你可以这么写:
CardSuit invalid1 = (CardSuit) 9000;
CardSuit invalid2 = CardSuit.Club | CardSuit.Diamond;
另外,考虑下面的情形。如果您需要扩展一个enum:
enum Title { Mr, Mrs }
Title枚举可以工作地很好,但一段时间后,如果需要添加一个“Ms”值,那么每一个switch语句都面临一个潜在的bug。当然您可以尝试修复所有的代码,却难免会发生遗漏。
枚举可以很好地表达某些概念,但是却无法提供足够的编译器检查。F#中的Union类型可设定为一组有限的值:数据标签(data tag)。例如,考虑一个表示微软员工的Union:
type MicrosoftEmployee =
| BillGates
| SteveBalmer
| Worker of string
| Lead of string * MicrosoftEmployee list
如果有一个MicrosoftEmployee类型的实例,您就知道它必定是{BillGates,SteveBalmer,Worker,Lead}之一。另外,如果它是Worker,您可以知道有一个字符串与之关联,也许是他的名字。我们可以轻松地创建Union类型,而后使用模式匹配(下一小节)来匹配它们的值。
let myBoss = Lead("Yasir", [Worker("Chris"); Worker("Matteo"); Worker("Santosh")])
let printGreeting (emp : MicrosoftEmployee) =
match emp with
| BillGates -> printfn "Hello, Bill"
| SteveBalmer -> printfn "Hello, Steve"
| Worker(name) | Lead(name, _)
-> printfn "Hello, %s" name
现在假设需要扩展Union类型:
type MicrosoftEmployee =
| BillGates
| SteveBalmer
| Worker of string
| Lead of string * MicrosoftEmployee list
| ChrisSmith
我们会看到一些编译器警告信息:
编译器检测到您没有匹配Union的每一个数据标签,发出了警告。像这样的检查会避免很多bug,要了解
更多的关于Union类型的信息,看这篇文章。
7. 模式匹配(Pattern Matching)
模式匹配看起来像是增强版的switch语句,允许您完成分支型控制流程。除了跟常数值进行比较外,还可以捕获新的值。比如在前面的例子中,我们在匹配Union数据标签时绑定了标识符“name”。
let printGreeting (emp : MicrosoftEmployee) =
match emp with
| BillGates -> printfn "Hello, Bill"
| SteveBalmer -> printfn "Hello, Steve"
| Worker(name) | Lead(name, _)
-> printfn "Hello, %s" name
还可以对数据的“结构”进行匹配,比如对列表(list)进行匹配。(还记得吗,x :: y表示x为列表的一个元素,y是x之后的元素,而[]则是空列表。)
let listLength aList =
match aList with
| [] -> 0
| a :: [] -> 1
| a :: b :: [] -> 2
| a :: b :: c :: [] -> 3
| _ -> failwith "List is too big!"
在这个匹配的最后,我们使用了通配符“_”(下划线),它匹配任意值。如果aList变量包含多于三个的元素,最后的模式子句将执行,并抛出一个异常。模式匹配还可以我们执行任意表达式来确定模式是否匹配(如果表达式的值为false,则不匹配)。
let isOdd x =
match x with
| _ when x % 2 = 0 -> false
| _ when x % 2 = 1 -> true
我们甚至可以使用动态类型测试进行匹配:
let getType (x : obj) =
match x with
| :? string -> "x is a string"
| :? int -> "x is a int"
| :? System.Exception -> "x is an exception"
| :? _ -> "invalid type"
8. 记录类型(Records)
在声明包含若干个公有属性的类型时,记录类型是一种轻量级的方式。它的一个优势是,借助于类型推演系统,编译器可以通过值的声明得出适当的记录类型。
type Address = {Name : string; Address : string; Zip : int}
let whiteHouse = {Name = "The White House"; Address = "1600 Pennsylvania Avenue";
Zip = 20500}
在上面的例子中,首先定义了“Address”类型,那么在声明它的实例时,无须显式地使用类型注解,编译器可根据字段(属性)的名称自行得出类型的信息。所以whiteHouse的类型为Address。
9. Forward Pipe Operator(|>)
|>操作符只是简单地定义为:
let (|>) x f = f x
其类型前面信息为:
'a -> ('a -> 'b) -> 'b
可以这么来理解:x的类型为'a,函数f接受'a类型的参数,返回类型为'b,操作符的结果就是将x传递给f后所求得的值。
还是来看个例子吧:
// Take a number, square it, then convert it to a string, then reverse that string
let square x = x * x
let toStr (x : int) = x.ToString()
let rev (x : string) = new String(Array.rev (x.ToCharArray()))
// 32 -> 1024 -> "1024" -> "4201"
let result = rev (toStr (square 32))
上面的代码是很直白的,但语法看起来却不太好。我们所做的就是将一个运算的结果传给下一个运算。我们可以通过引入几个变量来改写代码为:
let step1 = square 32
let step2 = toStr step1
let step3 = rev step2
let result = step3
但是我们需要维护这几个临时变量。|>操作符接受一个值,将其“转交”给一个函数。这会大大地简化F#代码:
let result = 32 |> square |> toStr |> rev
10. 序列(Sequence,System.Collections.Generic.IEnumerator<_>)
序列(在F#中为seq)是 System.Collections.Generic.IEnumerator的别名,但它在F#中有另外的作用。不像列表和数组,序列可包含无穷个值。只有当前的值保存在内存中,一旦序列计算了下个值,当前的值就会被忘记(丢弃)。例如,下面的代码生成了一个包含所有整数的序列。
let allIntegers = Seq.init_infinite (fun i -> i)
11. 集合(Collections:Seq,List,Array)
在F#中,如果您想表示一个值的集合,至少有三个好的选择——数组、列表和序列,它们都有各自的优点。而且每种类型都有一系列的模块内置于F#库中。您可以使用VS的智能感知来探究这些方法,这里我们来看看最常用的那些:
iter。“iter”函数遍历集合的每一项。这与“foreach”循环是一致的。下面的代码打印列表的每一项:
List.iter (fun i -> printfn "Has element %d" i) [1 .. 10]
map。像我在上篇文章中所说的,map函数基于一个指定的函数对集合的值进行转换。下面的例子将数组的整数值转换为它们的字符串表示:
Array.map (fun (i : int) -> i.ToString()) [| 1 .. 10 |]
fold。“fold”函数接受一个集合,并将集合的值折叠为单个的值。像iter和map一样,它接受一个函数,将其应用于集合的每个元素,但它还接受另一个“accumulator”参数。fold函数基于上一次运算不断地累积accumulator参数的值。看下面的例子:
Seq.fold (fun acc i -> i + acc) 10 { 1 .. 10 }
该代码的功能是:以10为基数(acculator),累加序列中的每一项。
只有序列有fold方法,列表和数组则有fold_left和fold_right方法。它们的不同之处在于计算顺序的不同。
12. 可选值(Option Values)
基于函数式编程的特点,在F#中很难见到null值。但有些情况下,null值比未初始化变量更有意义。有时可选值则表示值未提供(可选值就像C#中的nullable类型)。
F#中的“可选类型(option type)”有两种状态:“Some”和“None”。在下面的记录类型Person中,中间的字段可能有值,也可能没有值。
type Person = { First : string; MI : string option; Last : string }
let billg = {First = "Bill"; MI = Some("H"); Last = "Gates" }
let chrsmith = {First = "Chris"; MI = None; Last = "Smith" }
13. 延迟求值(惰性值,Lazy Values,Microsoft.FSharp.Core.Lazy<_>)
延迟初始化表示一些值,它们在需要时才进行计算。F#拥有延迟求值特性。看下面的例子,“x”是一个整数,当对其进行求值时会打印“Computed”。
> let x = lazy (printfn "Computed."; 42);;
val x : Lazy<int>
> let listOfX = [x; x; x];;
val listOfX : Lazy<int> list
> x.Force();;
Computed.
val it : int = 42
可以看到,我们在调用“Force”方法时,对x进行求值,返回的值是42。您可以使用延迟初始化来避免不必要的计算。另外在构造递归值时,也很有用。例如,考虑一个Union值,它用来表示循环列表:
type InfiniteList =
| ListNode of int * InfiniteList
let rec circularList = ListNode(1, circularList)
“circularList”拥有对自身的引用(表示一个无限循环)。不使用延迟初始化的话,声明这样类型的值是不可能的。