http://msdn.microsoft.com/zh-cn/magazine/cc164244.aspx#S3
使用 .NET Framework 中的函数式编程技术
Ted Neward
红色字体为重点,
黄色背景的胃tips
本文讨论:
| 本文使用了以下技术: .NET Framework, F# |
作 为 Microsoft
®
.NET Framework 家族的新成员,F# 提供类型安全、性能以及类似脚本语言的工作能力,所有这些都是 .NET 环境的一部分。此函数式语言由 Microsoft 研究院的 Don Syme 发明,作为 CLR 的 OCaml 语法兼容变体,但 F# 已经迅速地从科研转为投入实际应用。
随着函数式编程的概念通过 .NET 泛型和 LINQ 等技术越来越多地渗入主流语言(例如 C# 和 Visual Basic
®),F# 在 .NET 社区里的知名程度也不断提高——因此,2007 年 11 月 Microsoft 宣布将 F# 确定为受支持的 .NET 编程语言。
多年来,大家一直认为函数式语言领域(ML、Haskell 等)更适合用于学术研究,而不适用于专业开发。但这并不代表这些语言没有过人之处。事实上,.NET 的一些重要的功能增强(例如泛型、LINQ、PLINQ 和 Futures)都是将一些函数式编程概念全新应用到语言所致。以往对这些语言的关注程度不高主要是因为它们的目标平台与专为 Windows
®
编写程序的开发人员关系不大、不能与底层平台很好集成,或者不支持关系数据库访问、XML 解析和进程外通信机制等主要功能。
但是,CLR 及其“多种语言,单一平台”的方法将使此类语言在 Windows 开发中的应用越来越广泛。并且顺理成章地引起在一线工作的程序员们的注意。F# 即是这样一门语言。在本文中,我将为您介绍一些 F# 的基本概念和优点。然后,为了帮助您初步了解 F#,我将详细介绍它的安装过程并编写几个简单的小程序。
为什么要使用 F#?
对于小部分 .NET 程序员来说,学习一门 .NET Framework 函数化语言无疑将使自己在编写功能强大软件方面前进一大步。而对其他程序员来说,学习 F# 的理由就因人而异了。F# 能为开发人员提供哪些益处?
随着多核 CPU 的普及,安全并发程序已成为过去三年来的关注焦点。
函数式语言倡导一种固定不变的数据结构,可在线程和机器之间传递,而无需担心线程安全或原子访问,开发人员可以利用这一特点支持并发操作。函数式语言还可更轻松地编写更支持并发特性的库,如稍后将在本文中介绍的 F# 异步工作流。
尽管对于专攻面向对象开发的程序员而言,可能对这种语言感觉不是这么强烈,但在很多情况下,函数式程序确实可以简化某些应用程序的编写和维护。例如,编写一个将 XML 文档转换成其他格式数据的程序。虽然完全可以通过编写一个 C# 程序,让它解析整个 XML 文档并应用各种 if 语句确定在文档中的不同位置采取何种措施,但实际上更好的方法是编写可扩展样式表语言转换 (XSLT) 程序。当然,XSLT 肯定包含大量的内置函数机制,如同 SQL 一样。
F# 强烈建议不要使用空值 (null),而是提倡使用固定不变的数据结构。这些特性可以减少需要编写的特例代码量,从而有助于降低编程出错的频率。
使用 F# 编写的程序还更加简洁。您可以切实地从两方面减少键入的内容:击键次数更少并且必须要向编译器通告变量类型、参数或返回类型的位置点也更少。这意味着需要维护的代码将大大减少。
F# 具有与 C# 相似的性能特点。但是,与简洁程度相似的语言(特别是那些动态和脚本语言)相比,它的性能特点要好得多。并且,F# 也包含通过编写程序段并交互式执行查看数据的工具,这一点与许多动态语言类似。
安装 F#
F# 可从
research.microsoft.com/fsharp/fsharp.aspx
免费下载,它不仅会安装所有命令行工具,而且还会安装 Visual Studio
®
扩展软件包,该软件包提供彩色语法突出显示、项目和文件模板(包括作为入门指南的详细 F# 示例代码)以及 IntelliSense
®
支持。同时它还提供可在 Visual Studio 内部运行的 F# 交互式 shell,它使开发人员能够从源文件窗口中提取表达式、将表达式粘贴到交互式 shell 窗口,并立即得到代码段的结果——在类似增强的 Immediate 窗口中显示结果。
在我撰写本专栏时,F# 在 Visual Studio 内作为外部工具运行,所以它缺少某些开发人员能够从 C# 或 Visual Basic 中获得的无缝集成的能力。此外,F# 还缺少 ASP.NET 页面设计器支持。(这并不是说 F# 不能在 ASP.NET 中使用,完全不是。这仅表示 Visual Studio 并没有为 F# 提供类似 C# 和 Visual Basic 的那种现成拖放式开发体验。)
尽管如此,当前版本的 F# 还是可以在能够使用其他 .NET 兼容语言的任何位置使用。在接下来的几页中,您将看到一些示例。
您好,F#
介绍任何语言的特有方式就是通过那几乎成为标准的“Hello, World”程序。F# 也不例外:
printf "Hello, world!"
虽然不能引起您太大的兴趣,但这个很小的例子显示出 F# 属于不需要显式入口点(C#、Visual Basic 和 C++/CLI 都需要显式入口点)的语言;该语言假设程序的第一行即为入口点并将从此处开始执行。
要运行此程序,刚入门的 F# 开发人员有两个选择:编译或解释。在 F# 解释程序中运行此程序 (fsi.exe) 很简单。只需从命令行启动 fsi.exe 并在出现的提示中输入上面一行内容即可,如
图 1
所示。
图 1
在 F# 解释程序中运行 'Hello, World' (单击该图像获得较大视图)
请注意,在 shell 中,每条语句必须以两个分号结尾。这是交互模式的特殊要求,编译过的 F# 程序并不需要使用这种方式。
要将此示例作为标准的 .NET 可执行程序运行,请正常启动 Visual Studio 并创建一个新的 F# 项目(可以在“其他项目类型”中找到这个项目)。最初,F# 项目包含一个 F# 源文件,称为 file1.fs。打开此文件将发现大量示例 F# 代码集合。浏览一下这些内容即可大概了解 F# 的语法结构。然后将整个文件替换成前面所显示的 "Hello, world!" 代码。运行此应用程序,毫无疑问,在控制台应用程序窗口中将出现“Hello, world!”。
如果更喜欢命令行方式,可以使用 F# 安装目录中 \bin 子目录里的 fsc.exe 工具编译这段代码。请注意:fsc.exe 与大多数命令行编译器的工作方式相似,它从命令行获取源代码并生成可执行程序作为结果。大多数命令行开关都有相关文档,如果曾经使用过 csc.exe 或 cl.exe 编译器,那您可能已经熟悉其中许多开关。不过请注意,F# 目前对 MSBuild 的支持尚不完美;在当前安装版本(撰写本文时为 1.9.3.7)中不能直接支持由 MSBuild 驱动的编译。
如果希望“Hello, world!”中更具图形化特点,F# 可以通过 CLR 平台(包括 Windows Forms 库)轻松地提供完整的逼真度和互操作性。尝试以下代码:
System.Windows.Forms.MessageBox.Show "Hello World"
利用 .NET Framework 类库和 F# 库的能力使得 F# 语言不仅对那些早已使用如 OCaml 或 Haskell 之类函数式语言进行数学和科学计算的社区极具吸引力,而且受到全世界现有 .NET 开发人员的青睐。
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)); } }
关键字 For
现在让我们看一下第一个示例中的 for 关键字:
#light let results = [ for i in 0 .. 100 -> (i, i*i) ] printfn "results = %A" results
先看代码的顶部,注意 #light 语法。这是为非 OCaml 程序员开始使用 F# 而做的让步,放宽某些 OCaml 语言的语法要求,并使用大量空白定义代码块。虽然不一定必要,但对于那些原本使用 C# 或 Visual Basic 的普通开发人员来说,它确实使语法更易于解析,因此它常出现在 F# 示例和公开的代码段中,并已成为事实上的 F# 编程标准。(未来版本的 F# 可能会将 #light 确定为默认语法,代替其他类似方法。)
那个看似简单的 for 循环实际上并不简单。正式地说,这是生成列表,即将生成列表型结果的代码块的另一种说法。
列表是函数式语言中经常出现的一种原语结构,在这方面它与数组有许多相似之处。但是,列表不允许基于位置的访问(如 C# 中传统的 a[i] 语法)。列表可在函数式编程的不同位置出现,大多数情况中,可以将其看作是 F# 中与 .NET Framework 中的 List<T> 类似,但提供一些增强功能的同等项。
列表通常使用一些特殊类型,本例中标识符结果是聚合列表,特别地是 F# 将此聚合类型标识为类型 (int * int)。如果将此聚合列表看作是 SQL 中 SELECT 语句返回的一对列,它的含义就清楚多了。因此,示例本质上是创建一个包含 100 个项目的整数对列表。
通常,在函数式语言中,函数定义可以在出现代码的任何位置使用。因此,如果希望扩展前面的示例,可以编写以下代码:
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; // Methods public Vector2D(double dx, double dy) { Hello.Vector2D @this = this; @this._dx@48 = dx; @this._dy@48 = dy; double d = (@this._dx@48 * @this._dx@48) + (@this._dy@48 * @this._dy@48); @this._length@49 = Math.Sqrt(d); } public Hello.Vector2D Move(double dx2, double dy2) { return new Hello.Vector2D(this._dx@48 + dx2, this._dy@48 + dy2); } // Properties public double DX { get { return this._dx@48; } } public double DY { get { return this._dy@48; } } public double Length { get { return this._length@49; } } }
请记住,
F# 与大多数函数式语言相似,提倡使用不变的值和状态。当查看
图 3
中的代码时,这一点尤为明显,因为所有属性都为只读属性,并且 Move 成员不会修改现有的 Vector2D,而是从当前 Vector2D 创建新的副本,并在返回副本之前对其应用修改的值。
还请注意,
F# 版本不仅具备整体线程安全性,而且完全可以通过传统的 C# 或 Visual Basic 代码进行访问。这为 F# 入门提供了一种简便方法:使用它定义想要或者需要线程安全和固定不变的业务对象或其他类型。虽然完全可以在 F# 中创建提供常用可变操作组(设置属性及类似操作)的类型,但需要更多的工作,而且需要使用 mutable 关键字才可完成。在当今并发问题成为日常工作主旋律的世界中,这正如许多人所要求的一样——
默认固定不变,必需或想要时可变。
在 F# 中创建类型很有趣,但还是可以用 F# 去做那些传统 C# 或 Visual Basic 代码可以做到的工作,如创建简单的 Windows 窗体应用程序并从用户处收集输入,如
图 4
所示。
Figure 4 使用 F# 编写 Windows 窗体
#light 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 的函数,由它完成适当的操作。eg: ignore(capitals.ContainsKey(
"France"))
异步 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 printWithThread str = printfn "[ThreadId = %d] %s" Thread.CurrentThread.ManagedThreadId str 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) } let R = Async.Run awr printf "Result = %f\n" R
这些函数在 .NET 线程池以外执行再次表明 F# 语言对运行时互操作性的支持非常出色。甚至函数式语言中以前专门为特殊实现(如线程)保留的领域也要依赖 .NET Framework 类库,这表明
C# 程序员能够使用 F# 库或模块,就像 F# 开发人员能够使用 C# 库一样。事实上,将来 F# 的功能(如异步任务)能够充分利用新的 .NET Framework 库(例如并行扩展库中的任务处理库)。
与 F# 合作
如果对 F# 还不是十分清楚,那还有许多有关 F# 语言的介绍,这些内容远远超出一篇文章所能容纳的范围;事实上,在新语法和全新的思考方式(函数式与过程式)之间,坦白说对于已经习惯 C# 或 Visual Basic 等面向对象的普通开发人员来说,要掌握 F# 还需要一些时间。幸运的是,F# 与 .NET 系统中的其他语言可完全互操作,这表示您可以利用许多现有的知识和工具帮助您将 F# 变为自己的编程工具。
F# 开发人员可以访问所有的基类库,并且由于 F# 支持一些过程式和面向对象式开发,完全可以考虑不经过编译,使用 F# 交互模型学习 F# 语法和 Windows Presentation Foundation、Windows Communication Foundation 或 Windows Workflow Foundation 的细节。
如前所述,开发人员可以在 F# 中编写供应用程序代码其他部分使用的业务对象。因为 F# 类型构造生成的类大部分与使用 C# 或 Visual Basic 构造的类相似,所以 NHibernate 这类的持久型库仍可顺利保留 F# 类型,从而使 F# 能够无缝注入其他正在使用的业务应用程序中。
只需学习一些 F# 的知识就可帮助您更好地理解 C# 和 Visual Basic 未来版本中的许多新特性,因为其中许多想法和概念——包括泛型、迭代器(C# 中的 yield 关键字)和 LINQ——都来源于函数式和 F# 团队所做的研究。无论您怎么看待函数式编程,它都已是既成的事实,并且还将继续发挥功能。