2024年2月20日发(作者:)

1. F#与函数式编程概述

F#是微软.NET平台上一门新兴的函数式编程语言,通过函数式语言,开发人员可以轻松应对多核多并发时代的并行计算和分布问题。本文是F#简明教程的第一章,带您走进F#和函数式编程。

F#是微软.NET开发平台的一门编程语言,其最大的特点是对函数式编程(FP,Functional Programming)的引入;F#对面向对象(OOP)编程的支持也很出色,使用F#语言,开发人员可以自由选择函数式编程或面向对象编程来实现他们的项目。此外,F#还可以与.NET平台上C#、VB等其他编程语言紧密结合。

CPU多核心化和云计算的背景下,函数式编程可以很好的解决多并发运算的问题(在处理并发问题方面,面向对象编程存在一定程度的固有缺陷,比如类和实例化过程中产生的一些副作用,详细请参考对另一门函数式编程语言Erlang的视频访谈《因并发而生 因云计算而热:Erlang专家访谈实录》)。微软看到了这个趋势,试图通过专门为函数式编程打造的F#语言提升.NET平台在并发处理、多核多并发方面的能力,进一步提升开发人员的生产力和代码运行效率。

在2009年的TechED上,就F#和函数式编程的问题视频采访了微软MVP赵颉老师,我们可以采访视频了解F#和函数式编程最近的发展,详细请参考《TechED 09视频专访:F#与函数式编程语言》。

F#小背景:看似年轻的F#已经有近10年的历史。最初由微软研究院的Don Syme于2002年立项研发;F#在2005年推出第一个版本,2007年底,微软宣布F#进入产品化阶段。在不断的改进中,F#从C#、Linq和Haskell中吸收了很多优点。

1.1 F#编程起步

F#可以运行在.NET Framework 2.0版本以上的平台。如果你的Visual Studio之前没有安装F#,可以从微软F# Developer Center获得

(/en-us/fsharp/)。

不能免俗,让我们来看看F#的Hello World代码:

#light

ine(“This is one hello”)

printfn “This is another hello”

将代码保存为文件后,我们需要在命令行中通过编译生成一个.NET程序集。在命令行中的编译代码如下:

fsc

通过上面的代码,我们就得到了常见的可执行文件(.exe文件),这就是我们F#的起步——。

F#小提示:F#是.NET平台上的一个编译型语言,但仍然可以像脚本语言一样运行。可以使用Visual Studio或(在F#安装目录下的bin目录)进行F#脚本的执行。

1.2代码解读

让我们来仔细看看文件里的代码

◆ 程序首先以“#light”开始,在以后的F#之路上,我们会经常看到“#light”;大多数时候,“#light”总是出现在F#程序的开始位置,这是F#轻量级语法的标识;在最新的F#版本中,#light将作为默认选项。

◆ “ine”调用一个.NET基础类(熟悉C#或的朋友会相当熟悉)用来初始化一些必要的功能。

◆ “printfn”是F#的一个常用函数,他会将双引号中的参数输出到控制台上显示。

跟其他程序的Hello World一样,这段F#代码简单易懂,看着跟其他语言写就的Hello World还有些相似;但作为函数式编程语言,F#的语法和编程中的思路却有很大的不同。在下周的章节中,我们将深入F#编程,讲解F#的类型系统及编译机制。

2. F#类型系统和类型推断机制

F#是一种类型推断语言,它们的类型在编译过程中被推断和确定;这与Java或C#中的

泛型基本相似,本节教程我们将介绍F#的类型系统和类型推断机制,这是理解F#的基础。

在上一篇教程《F#与函数式编程概述》中我们了解到F#和函数式编程的一些特点,更多关于F#语言和函数式编程的介绍可以参考51CTO之前对微软MVP赵颉老师的专访《TechED 09视频专访:F#与函数式编程语言》。本节教程我们将学习到F#的一些基础原理,在开始之前,让我们先温习一下我们的Hello World代码:

#light

ine(“This is one hello”)

printfn “This is another hello”

F#是函数式和面向对象的混合体。它有时候会看起来与C#或Visual Basic惊人的相似,但却又完全陌生。F#程序以一系列的表达式形式组成,每个表达式可以通过“let”标识符被指定,比如:

let fles = oryInfo(@”C:UsersChance”).

GetFiles()

在上面的代码中,“fles”被指定了一个值,在这个例子中,是一个文件路径。有意思的是,程序运行中,直到语句在得到右侧的返回值前,“fles”的实际类型都没有被详细定义。你可能觉得有些别扭,在Java或其他编程语言中,变量fles应该被定义成一种数据类型,string或是其他什么类型以在内存中可以明确的被编译器区别对待,但这些规则在F#中有些不同。这也导致我们的F#简明教程稍有不同,我们不会像通常的教程那样介绍F#的基本数据类型,从某种意义上说,F#可以是任意类型或只有一个类型。

F#小提示:F#是一种类型推断语言,它们在编译过程中被推断和确定。如果你在Visual

Studio中编写F#,将鼠标指向某个值就会得到它的类型,编译器可以通过函数体或其他方式的定义推断出类型;Visual Studio是开发F#的主要工具,51CTO推荐您阅读Visual Studio

2010中关于F#的资源一文。

2.1 类型推断(Type Inference)

我们说数据的类型是被推断出的,因为F#的编译期进程会试图根据变量自身的特点来判断出它的类型并确保这种类型是安全的。尽管F#是强类型语言,但变量的类型声明在类型的判断推理过程中并不是必须的。

类型推断有自身的优点。在使用F#开发一些大型应用时,比如.NET和Java开发者都很熟悉的泛型特性(Generics)便是由类型推断来完成。注意,F#编译器会视任何没有类型标注的表达式为泛型。例如,下面的函数中,各变量的类型被定义(推断为)泛型,即使程序编写者没有定义任何类型。

let f x =

let y = g x

h y

let f (x:’a) : ’b =

let y:’c = (g:’a->’c) x

(h:’c->’b) y

F#小提示:在F#中,泛型类型参数是一个以撇号为前缀的字符。比如上面例子中的’b和’c就是最常用的泛型参数。像在.NET中一样,泛型类型也可使用尖括号语法,比如“Dictionary<’Key,’Value>”。只有一个泛型参数的时候,你有时候会看到它使用‘前缀’语法而不是尖括号——最常见的是和F#泛型类‘list’和‘option’一起使用。比如“int

list”和“list”表达同一种功能,只是书写方式不同。

2.2 F#类型推断机制

F#语言中的大多数类型推断可以遵循以下两条规则。首先,如果一个函数用于产生一个值,编译器将假定该值的类型是函数需求的。第二,如果一个值是一个表达式的必然结果,这个值的类型是这个表达式所决定的。

有些情况下这些简单的规则不够完全,编译器必须需要类型声明。比如,当一个算数运算符被使用,F#会处理的非常谨慎,如果没有程序员的明确代码,不会将一个数值型赋予另一个。这样做是为了确保F#在进行大规模数值计算时,类型推断不会加重编译器的负担。

针对第二条规则的例子在方法过载的情况下发生。比如Write方法在e(.NET中e封装了基于控制台应用程序的输入、输出和错误流操作)中有18个负载。类型推断可以确定传送给它的类型,但是无法确定另一个方向传送的值的类型。

类型推断不只是简单的符号,它还可以用于程序功能的检测。当你写了一段代码,类型推断功能为这些代码智能的获得了指定的类型,这意味着错误不会被引入程序。这种机制使F#获得动态语言的代码简洁性的同时保证了完全静态的类型系统。

更多关于F#的类型和语法基础请参考:

◆ F#数据类型:Discriminator Union

◆ F#基本语法,模式匹配及List

F#的类型系统和类型推断机制是学习和理解F#语言的基础,掌握了这些有利于我们之后的学习。下周我们将继续F#的学习,一起探究F#的基础语法。

3. 基本语法,模式匹配及List

F#是随着VS2010 Beta版一起正式推出的另一个基于.NET平台的语言,以函数式语言著称。这个F#入门文章介绍了F#的基本语法,模式匹配及List基本类型。

F#随着VSTS 2010 Beta1 发布也有一段时间了,园子里应该也有不少人对它

感兴趣吧。下面的例子是我在学F# 基本语法时写的一个简单Sieve of

Eratosthenes 实现,通过剖析这一小段代码,我希望大家能对F#有个简单认识,并能自己写一些简单的小程序。

3.1 F#入门代码

let GetAllPrimesBefore n =

let container = (n+1) 0

let rec loop acc = function

|[] -> acc

|hd::tl ->

if container.[hd] =1 then

loop acc tl

else

for j in [hd .. hd .. n] do

 container.[j] <- 1

 loop (hd::acc) tl

 loop [] [2 .. n]



 let primesBefore120 = GetAllPrimesBefore 120

进入正题

let GetAllPrimesBefore n =

第一行,申明函数GetAllPrimesBefore, 并且该函数有一个参数n, 在这里我没有指定n的类型,因为编绎器可以通过函数体对n的类型进去推断,比如在本例中,n就是int类型,当然我们也可以显示的指定n的类型,比如 let

GetAllPrimesBefore (n:int),这样我们就指定了n为int型 (注意:(n:int)中的括号不能省略,let GetAllPrimesBefore n : int 的意思是该函数返回的值的int型)。说完了参数,再说下返回值,同样,编绎器会根据函数体上下文对返回值类型进去推断,所以我们不需要申明返回类型。

let container = (n+1) 0

第二行,首先请注意该行与第一行相对有一个缩进({TAB}),F#和Python一样,也是通过{TAB}缩进来组织代码结构的。这一行我们定义了一个变量container,它的类型是Array,大小为 n+1, 并且值全部初使化为0

接下来就是这个函数的主要部分了,首先我们定义了一个递归函数(我们发现

定义递归函数需要加rec关键字)。它接受两个参数,acc和一个List,有朋友可能要问了,这里明明我只看到一个参数acc,你说的那个List在哪呢?可能有细心的朋友也发现了这里的函数定义不光前面有rec,在等号后面还加了个function,那么function是做什么用的呢?

3.2 F#入门:模式匹配

这里我需要首先讲一下Pattern Matching(模式匹配), Pattern Matching有些类似于C#中的switch语句(当然它要比C#中的switch强大许多,但这不是本文的目地,所以略去不表),可以根据expr的值去执行某一具体分支,它的基本语法也很简单,我们还是结合一个具体实例来看一下(例子比较简单,只是为了说明问题)。 这个例子大家很容易看懂吧,我就不详细解释了,只是说明一点,'_'用来匹配所有别的情况。

let ShowGreeting laguageInUse =

match laguageInUse with

| "C#" -> printfn "Hello, C# developer!"

| "F#" -> printfn "Hello, F# developer!"

|_ -> printfn "Hello, other developers!"

因为Pattern Matching在F#中的使用范围实在太广了,所以就引入了一种简化版,这就是上面大家看到的等号后面的function的作用,我们可以把上面的例子简化成

let ShowGreeting = function

| "C#" -> printfn "Hello, C# developer!"

| "F#" -> printfn "Hello, F# developer!"

|_ -> printfn "Hello, other developers!"

怎么样?既少了给参数起名的烦恼,也少敲不少字吧,嘿嘿。

3.3 F#入门:List基本类型

接下来我再简单介绍下F#中非常重要的一个基本类型List, 其基本表示形式为 [ item1;item2; .. ;itemn]

F#中List是immutable类型,我们只能访问里面的值,不能改动里面的值,任何改动List的需求只能通过构建新的List来实现。稍一思考,大家就会很快发现要实现一个高效的immutable list, 那最简单的就是对其头结点进去操作了(插入和删除都可以达到O(1),当然插入和删除会构建一个新的List,原List不会改变),F#中的List也是基于这种形式,所有的List都可以看成是Head+Tail(除了Head外的所有结点),F#提供了相应的库函数, ,并且提供了:: (cons

operator)来帮助我们方便的构建一个List,比如1::2::[]就表示List [1;2] (注意1和2之间我用的是;不是, 如果写成[1,2],那个表示该List只有一个元素 (1,2),至于(1,2)是什么类型,为了使文章尽量紧凑,我们今天就不讲了)

有了上面这些知识,再看本文一开始的函数就简单多了

let rec loop acc = function

|[] -> acc

|hd::tl ->

if container.[hd] =1 then

loop acc tl

else

for j in [hd .. hd .. n] do

container.[j] <- 1

loop (hd::acc) tl

首先,该函数的第二个参数是List,当List为空时,就把acc反序返回,当List不为空时,把List分成两部分(hd::tl),检查当前值n (n的值等于td) 是否己被标记。如果己经被标记(container.[hd] =1),略过当前值,检查接下来的值 loop acc

tl;如果没有被标记(当前值是素数),用当前值和acc构建一个新List (hd::acc),并对当前值的所有倍数进去标记(for loop),然后检查下一个值 loop (hd::acc) tl

这里有两点需要特别说明一下:

1. container是一个Array类型的参数,Array在F#中是mutable类型的容器,我们可以修改里面的元素,访问元素用Array.[i], 修改元素用Array.<-[i] =

newValue(不要忘记中间的.)

2. for loop的基本形式为 for in do, 我们可以使用[start .. end]或[start .. step .. end]来构建一个range,当然,这里的range其实也是一个List

看完了内部函数,我们再接着往下看

loop [] [2 .. n]

这里就很简单了,调用我们刚刚定义的内部函数,(acc为空List [], 第二个参数为List [2 .. n]),其返回值(List acc)就是函数GetAllPrimesBefore的返回值,F#中函数有返回值时不需要敲return。函数调用也很简单,(不需要在参数与函数名之间加括号)

后记

1. F#中函数体内可以定义新的值,变量和函数。(只在当前函数体内可见)。当然,这样做的好处显而易见,我就不啰嗦了。

2. Recursive function是functional programming中很常用的一种算法实现方式。functional programming language往往会针对尾递归进行特别的优化,F#也不例外,所以我们需要尽可能的把递归写成尾递归的形式,这个有时就需要像本文一样借助accumulator来实现。

4. F#数据类型:Discriminator Union

本文介绍了一个十分重要的F#数据类型:immutable的Discriminated Unions。它表示一组有限的可选情形,并且每种情形都有自己的严格定义。

4.1 F#数据类型之Discriminator Union简介

我们通过一个简单的例子了解了怎样在F#中声明变量,定义函数,并且用到了F#中两个重要的数据类型List和Array,今天我主要介绍F#中一个非常重要的

immutable数据类型Discriminated Unions。还是首先看一个例子,这是我写的一个简单的生成二分查找树的例子。

type Tree<'a> =

| Node of 'a * Tree<'a> * Tree<'a>

| Nil

let generateBinarySearchTree l =

let rec insert a = function

| Node(root,left,right) when a < root -> Node(root, (insert a

| Node(root,left,right) when a > root -> Node (root,left, (insert

| Nil -> Node(a, Nil,Nil)

left), right)

a right))



 let rec loop acc = function

 |[] -> acc

 |hd::tl -> loop (insert hd acc) tl



 loop Nil l



 let tree1 = generateBinarySearchTree [5;3;9;4;6;7]

我们首先来看前三行,没错,这就是今天要重点介绍的F#数据类型:Discriminator Union

type Tree<'a> =

| Node of 'a * Tree<'a> * Tree<'a>

| Nil

首先注意到我们这次使用的是type,而不是前面常用的let关键字。 F#中使用type关键字来定义用户自定义类型,在这里我们定义了一个类型Tree, 那么Tree后面的<'a>又是啥意思呢?可能有的朋友己经猜到了,它表示a是一个泛型占位符,在实际使用中,a可能是int型,也可能是string等等(注意别忘了a前面的单引号)。后面二行就是具体的Tree定义了,它表示我们定义的Tree有两种可能,有可能是Node,也有可能是Nil。 我们先来看第一种情形

Node of 'a * Tree<'a> * Tree<'a>

它表示Node的类型是 'a * Tree<'a> * Tree<'a>, 那么这个又表示什么呢?其实它是F#中另外一种重要的immutable类型Tuple, Tuple很容易理解,它表示把

一个数据集合在逻辑上看作是一个整体。看个例子大家就明白了(注意分隔符是逗号)

let s2 = (1,"hello")

(在这里我们定义了一个类型为int * string的Tuple. 要使用它里面的值也很简单,我们可以声明新的变量并用s的值来初始化它们。 let i,s = s2就表示我们声明了int型变量i,它的值为1, string型变量s,它的值是2)

回到我们的例子中来, 'a * Tree<'a> * Tree<'a> 就很容易理解了,因为在定义Discriminated union时可以递归引用自己。

Tree的第二种情形Nil很简单,它表示一个什么都没有的空结点.

通过我上面详细的解释,我想大家也明白了什么是Discriminated union, 它表示一组有限的可选情形,并且每种情形都有自己的严格定义。回到我们上面的例子,Tree有两种情形,要么是 'a * Tree<'a> * Tree<'a>的Node,要么是一个空的Nil。大家也看到了它和Pattern matching结合使用非常频繁,这下明白为什么叫Discriminated union了吧

如果你认真读到上一篇文章的话,接下来构建二分查找树的代码比较简单,我就不解释了。我们接下来看如何判断某一个值是否在一个构建好的二分查找树中。

let rec tryFind x = function

| Node(root,_,_) when x = root -> Some(x)

| Node(root,left,_) when x tryFind x left

| Node(root,_,right) when x > root -> tryFind x right

| _ -> None

首先要注意我使用了五个'_',前面四个看起来好象和最后的一个有些不一样。记得我在上一篇中说过'_'用在Pattern Matching中用来匹配所有别的情况,而且我说过F#里的Pattern Matching要比C#中的Switch强大,在这里我们就看到了它的强大之处,它可以在找到匹配后,为匹配的各部分绑定一个变量名来方便我们后面的调用,在绑定时如果我们仅仅对某些部分感兴趣,那么我们就可以使用

'_'来代替我们不感兴趣的部分(注意'_'只能绑定一个对应部分,要对应两个我们就要敲两个'_','_').

其次我们注意到tryFind的返回值好象有两种情况呀,Some(x)和None,一个函数怎么能返回两种不同类型的值呢? 呵呵,忘记我们今天主要在讲Discriminated union了?这是个F#里事先定义好的一个discriminated union,它有自己的名字叫Option,它的定义非常简单,有了前面的基础,这个就不需要我解释了吧。

type option<'a> =

|Some of 'a

|None

总结:今天我主要说了F#中非常重要的一种immutable类型Discriminated

union,并顺带说了下另两个简单的类型Tuple和Option。简单的functional

programming知识就剩下最重要function没有说了,下一篇我主要来说说F#里的函数,希望在下一篇后,大家不再觉得F#难懂难用了。

5. F#语法精要

F#基本语法的学习是基于对F#类型系统和类型推断机制的理解的。在本节教程中,我们将学习到F#的变量声明和程序流程结构。

在上一节F#教程中,我们对F#的类型系统和类型推断机制有了一个初步的认识。F#的类型推断原理是学习F#的重要基础。本节课程,我们将在F#类型基础上进一步学习F#的一些基本语法。

“let”表达式是F#语法的核心,可以用作定义函数、序列等多种用途。另外,F#使用空格来标记程序块的开始与结束。

定义值

 let x = 2

定义函数值

 let f a = a + x

定义循环函数

open

let rec printSubDirFiles dir =

let fles = es dir

let dirs = ectories dir

printf “%sn%Ann” dir fles

printSubDirFiles dirs

此外,F#还提供传统的循环和迭代等流程控制结构,比如if、for、while。但我们需要注意的是,F#中的“if„then”和“if„then„else”与传统的面向对象语言有些不同。在F#中,大多数表达式必须含有一个值,并且控制结构“if„then„else”表达式的两边的值必须是同一类型。注:F#的这种语法约定源自其推断型语言的编译机制,详细请参考上一节教程中关于F#类型推断机制的介绍。

F#中的常用流程控制语句示例

与大多数.NET平台上的编程语言相似,F#也提供一些组织代码的机制。事实上,F#提供模块和命名空间两种方式,下面的一些演示将给出C#和VB的命名空间。F#的模块化不只局限与语法范围,还提供模块化的层级标准,例如集合和函数。

F#的基础代码组织:命名空间、类型和模型

namespace MyFSharpProg

open

type Foo () =

member uest =

module Main = begin

// values and functions here

end

与传统的函数式编程原则相同,多数时候,F#的标识符是不可变的。但F#允许定义和修改使用“mutable”保留字的值,或通过“ref”保留字改变其前面的引用。mutable的值可以通过左箭头操作(“<-”);ref的值可以通过“:=”操作符制指定。我们可以通过“!”获取ref的值。下面来看具体示例:

声明/更新可变值

let mutable x = 0

x <- x + 1

声明/更新参考值

let x = ref 0

x := !x + 1

F#小提示:在习惯了C#或Java等编程语言后,刚刚开始F#编程,阅读F#代码感觉就像乱码一样。因为F#为了保有函数式编程的一些优秀特质,不得不引入一些如“<-”、“:=”、“!”等奇怪的符号作为操作符或运算符;另外,F#在代码中需要通过一些推断机制来评判变量的类型,在阅读F#代码时,应对F#的类型系统做到心中有数,所以,多数时候我们看到的是“let”,而不是传统的“int”、“string”、“float”等。希望大家能充分理解F#的类型系统和类型推断机制,这是F#的重要基础,也是走进函数式编程语言的重要一步。