【F#2.0系列】F#入门(1)
创建第一个F#程序
程序目标:找出一个字符串中的重复单词
/// Split a string into words at spaces
let splitAtSpaces (text: string) =
text.Split ' '
|> Array.toList
/// Analyze a string for duplicate words
let wordCount text =
let words =
splitAtSpaces text
let wordSet = Set.ofList
words
let numWords =
words.Length
let numDups =
words.Length - wordSet.Count
(numWords,numDups)
/// Analyze a string for duplicate words and display the results.
let showWordCount text =
let numWords,numDups =
wordCount text
printfn "--> %d
words in the text" numWords
printfn
"--> %d duplicate words" numDups
上述源代码可以直接在F# Interactive执行。你可以通过F#套件中的fsi.exe来启动该环境,或直接使用Visual Studio.如果通过命令行,需使用;; 做为执行程序的标识。
C:\Users\dsyme\Desktop> fsi.exe
Microsoft (R) F# 2.0 Interactive build 4.0.30319.1
Copyright (c) Microsoft Corporation. All Rights Reserved.
For help type #help;;
> <paste in the earlier program here> ;;
val splitAtSpaces : string -> string[]
val wordCount : string -> int * int
val showWordCount : string -> unit
F# Interactive报告了上述程序的定义的函数splitAtSpaces, wordCount和showWordCount的类型。val关键字表示值 (value);在F#的世界里,函数都是值。
现在我们就可以执行wordCount这个方法了:
> let (numWords,numDups) = wordCount "All the king's horses and all the king's men";;
val numWords : int
val numDups : int
> numWords;;
val it : int = 9
> numDups;;
val it : int = 2
> numWords - numDups;;
val it : int = 7
上述代码展示了如何执行wordCount函数并且将其两个返回值绑定(binding)到numWords和numDups。 并且展示了如何单独显示每个返回值。
showWordCount可以将结果直接输出出来而不是返回值:
> showWordCount "Couldn't put Humpty together again";;
--> 5 words in the text
--> 0 duplicate words
程序非常简单。需要注意的是,F#使用XMLDoc进行注释,可以参照程序的第一行。
使用let
let是F#中最为重要的单个关键字(keyword),它用来:定义数据,计算值,函数和过程(procedure)。let绑定表达式的左边一般是一个标识符 (identifier),但也可以使一个匹配(pattern)。它也可以是一个函数名+参数名(argument names)。在wordCount这个例子中,其使用了一个参数text。let表达式右边 (以=分隔左右)是一个表达式(expression)。
值与可变性
与其他语言不同,F#中的本地即时值(immediate value of local)在其初始化之后默认不可改变,除非其显式的标识为mutable。也是因为这个特性,在F#中,我们使用“值”(value)来代替“变量”(variable)来描述。
虽然我们提供了mutable关键字,但是F#中大量的值与数据结构都是完全不可变的(immutable)。例如所有的.NET 基础类型integers, strings 和System.DateTime都是不可变的,并且F#中定义了一批不可变数据结构例如Set和Map。
不可变值(immutable values)意味着你不需要考虑值的对象标识(object identity),这可以在多线程环境避免冲突与锁。我们需要转换思维,去适应这种新的编程模式。
理解类型
F#是强类型语言。在我们定义wordCount的时候,系统返回了所有相关的类型:
val wordCount : string -> int * int
上述信息表示wordCount使用了一个string类型的参数,并且会返回int*int,在F#中意味着一对数字,也就是会返回两个int类型的值。
但值得注意的是,在我们定义wordCount的时候,参数text并未明确指定其类型,这是因为wordCount的全部类型是从其定义中推到而来的。也就是说,text的类型推到自splitAtSpaces,而splitAtSpaces的参数类型为string,所以text的类型也为string。在我看来,这种类型推导,可以看做将类型从方法签名中移除,函数间的互相引用只需提供相同个数的参数,不必关心其类型,系统会自动推导。这带来了维护性的便利。
强类型的使用主要是基于性能和可共用性(interoperability)考虑。F#中的类型系统简单并强大,因为其使用了纵横双向的(orthogonal),可组合的构造函数例如元组(tuple)和函数(function)来构建简洁的(succinct)和自描述(descriptive)的类型。更强大的是,类型推导(type inference)的引入甚至意味着你几乎不需要在程序中显式的声明类型。
重要的类型
一些类型的构造函数,例如list和option,是支持泛型的。你可以使用前置标记法(prefix notation)例如int list,或后置标记法(postfix notation) 例如list<int>来表示。
理解类型推导
类型推导(type inference)是通过分析你的源代码,收集约束(constraints)也就是类型限制来推导出所有的类型。F#编译器提供类型检查。类型限制的收集是从上到下,从左至右,从外至内的。
在你的代码是可重用的并且可以泛型化的时候,类型推导会自动将你的代码泛型化(automatically generalizes)。这个机制是简洁的可重用的强类型语言的关键。
函数调用
函数是绝大多数F#编程的核心。
在F# Interactive中:
> splitAtSpaces "hello world";;
val it : string list = [ "hello"; "world" ]
如上例所示,使用函数名后跟参数的方式即可调用函数。
简洁的语法
F#编译器及Interactive使用源代码缩进 (indentation) 来确定每个语句的开始与结束。缩进的规则是非常直观的。
下面看一个使用“in”关键字显示声明的wordCount函数:
/// Analyze a string for duplicate words
let wordCount text =
let words = splitAtSpaces text in
let wordSet = Set.ofList words in
let numWords = words.Length in
let numDups = words.Length - wordSet.Count in
(numWords,numDups)
依然需要使用双分号(;;)来在F# Interactive中标识输入结束。
也可以写在同一行中:
let powerOfFour n =
let nSquared = n * n in nSquared * nSquared
调用:
> powerOfFour 3;;
val it : int = 81
编译器最终都会把代码解释为let pat = expr1 in expr2的形式,其中pat代表匹配(pattern),expr1 和 expr2 表示表达式(expression)。编译器会把与let关键字纵向对齐的(column-aligned)代码前插入in关键字。
F#缩进不支持tab。不过不用担心,大多数的编辑器会在F#环境中自动将tab转换为空格。即便不支持,也会显式的报错。
理解作用域(Scope)
使用let定义的变量的作用域为整个表达式,但不包含定义本身。let的定义为顺序的,从上至下的。
几种错误的使用方式:
let badDefinition1 =
let words = splitAtSpaces text
^^^^ error: text is not defined
let text = "We three kings"
words.Length;;
let badDefinition2 = badDefinition2+1
------------------^^^^^^^^^^^^^^^
stdin(21,19): error FS0039: The value or constructor 'badDefinition2' is not defined
可以通过定义同名值(value)的方式模拟作用域范围外使用。例如我们需要计算(n*n*n*n)+2:
let powerOfFourPlusTwo n =
let n = n * n
let n = n * n
let n = n + 2
n
上述代码和以下代码相同:
let powerOfFourPlusTwo n =
let n1 = n * n
let n2 = n1 * n1
let n3 = n2 + 2
n3
值得注意的是,暴露(outscoping)一个值并不会改变它的原始值;简单的说,我们只是通过在其作用域外重新定义同名变量的方式模拟了作用域外访问。
通过缩进定义其作用域:
let powerOfFourPlusTwoTimesSix n =
let n3 =
let n1 = n * n
let n2 = n1 * n1
n2 + 2
let n4 = n3 * 6
n4
上述代码的n1和n2便无法再n4表达式中访问。
看一个错误的例子:
let invalidFunction n =
let n3 =
let n1 = n + n
let n2 = n1 * n1
n1 * n2
let n4 = n1 + n2 + n3 // Error! n3 is in scope, but n1 and n2 are not!
n4
上述错误的代码表示了作用域的判定原则。就像之前说过的,非常直观。