第三章 函数编程(三)

728 篇文章 1 订阅
86 篇文章 0 订阅

第三章 函数编程(三)


控制流(Control Flow)

 

F# 有很强大的概念,控制流。它和其他许多纯函数语言不同,控制流的概念很松散,本质上,表达式可以用任何顺序计算。

控制流的强概念以if ...then ... else ... 表达式的形式出现 。

在F# 中,if ...then ... else ... 结构是表达式,说明它返回值。根据 if 和 then 关键字的逻辑表达式,返回两个不同值中的一个。下面的例子演示这一点。计算if ...then ... else ...表达式的值,返回 "heads" 或  "tails",取决于程序运行在偶数或奇数秒。

 

let result =

if System.DateTime.Now.Second % 2 = 0 then

"heads"

else

"tails"

printfn "%A" result

 

注意到没有,if ...then ... else ... 表达式很有趣,它实际上就是针对布尔值模式匹配的快捷方式。因此,前面的示例可以改写成:

 

let result =

 match System.DateTime.Now.Second % 2 = 0 with

  |true -> "heads"

  |false -> "tails"

 

如果你更熟悉命令风格编程的话,可能不希望 if ...then ... else ...  表达式有一些限制。F# 的类型系统要求 if ...then... else ... 表达式返回的值必须有相同的类型,否则,编译就会出错。因此,在前面的例子中,如果把字符串"tails" 替换为整型或布尔型的值,就会编译出错。如果确实需要返回不同类型的值,可以创建 obj (System.Object 的F#版本)类型的if ...then ... else ... 表达式。下面的例子就是这样做的,输出 "heads" 或者 false 到控制台。

 

let result =

  ifSystem.DateTime.Now.Second % 2 = 0 then

    box"heads"

  else

    boxfalse

printfn "%A" result

 

命令式编程人员还有一个可能会很奇怪的地方,就是if … then … else … 表达式如果返回值,必须有 else。你考虑一下,再看看前面的例子,在逻辑上是相当好的。如果从代码中去掉else,当 if 为 false时,标识符result 就不会被赋值,F# (以及通常的函数编程语言)都力图避免未初始化的标识符有内容。有一种编程方法,只包含if ... then 而没有else 的表达式,但它更像命令编程的风格了,我们在第四章会有讨论。

 

 

列表(lists)

 

F# 列表是内置的简单集合类型。可以是一个空列表,用中括号表示([]),也可以是把值级连到其他的列表。把值添加到列表的前面,用内置的运算符(::),读着“cons。下面的例子定义了一些列表,第一行是空列表,后面两行列表把字符串级连前面:

 

let emptyList = []

let oneItem = "one " :: []

let twoItem = "one " :: "two" :: []

 

通过级连的方式把内容添加到列表比较麻烦,因此,如果想定义一个列表,可以用简化的方法。

简写语法,把列表项放在中括号([])中,用分号(;)隔开,像这样:

 

let shortHand = ["apples ";"pairs "]

 

关于列表,F# 还有另一个运算符(@),用来级连两个列表,像这样:

 

let twoLists = ["one, ";"two, "] @ ["buckle "; "my "; "shoe "]

 

F# 列表中的所有项必须类型相同。如果在一个列表中放入不同类型的项,比如,级连字符串到整型的列表,会产生编译错误。如果需要混合类型的列表,那就应该创建obj 类型(F# 中System.Object)的列表,如下面的代码。

 

// the empty list

let emptyList = []

// list of one item

let oneItem = "one " :: []

// list of two items

let twoItem = "one " :: "two" :: []

// list of two items

let shortHand = ["apples ";"pairs "]

// concatenation of two lists

let twoLists = ["one, ";"two, "] @ ["buckle "; "my "; "shoe "]

// list of objects

let objList = [box 1; box 2.0; box"three"]

// print the lists

let main() =

 printfn "%A" emptyList

 printfn "%A" oneItem

 printfn "%A" twoItem

 printfn "%A" shortHand

 printfn "%A" twoLists

 printfn "%A" objList

 

// call the main function

main()

 

下面是这个例子的结果:

 

["one "]

["one "; "two "]

["apples "; "pairs "]

["one, "; "two, ";"buckle "; "my "; "shoe "]

[1; 2.0; "three"]

 

在这一章的后面“类型和类型推断”一节会有更多有关类型的讨论。

F# 列表是不可改变的(immutable;),换句话说,列表一旦创建,就不能更改。作用于列表的函数、运算符不能更改列表,但可以创建一个新的、更改后的列表副本,原来的列表保持不变,如果需要仍然可用。下面就是这样一个例子。

 

// create a list of one item

let one = ["one "]

// create a list of two items

let two = "two " :: one

// create a list of three items

let three = "three " :: two

// reverse the list of three items

let rightWayRound = List.rev three

// function to print the results

let main() =

 printfn "%A" one

 printfn "%A" two

 printfn "%A" three

 printfn "%A" rightWayRound

// call the main function

main()

 

先创建一个F# 列表,只包含一个字符串,然后,再创建两个列表,后面一个都是以前面一个作为基础,最后,函数List.rev 应用于最后的列表,创建一个新的反转列表。当你输出这些列表,很容易发现所有的原始列表都保持不变:

 

["one "]

["two "; "one "]

["three "; "two ";"one "]

["one "; "two ";"three "]

 

 

根据列表的模式匹配

 

处理F# 列表的常规工作方法是使用模式匹配和递归。用于从列表中取出头的模式匹配的语法与级连列表项的语法相同,模式有组成是这样的,表示头的标识符,后面是 ::,然后是表示列表其余部分的标识符,下面例子中 concatList 的第一个规则就是这样的。也可以根据列表常量进行模式匹配,看 concatList 的第二个规则,是一个空列表。

 

// list to be concatenated

let listOfList = [[2; 3; 5]; [7; 11; 13];[17; 19; 23; 29]]

// definition of a concatenation function

let rec concatList l =

 match l with

  |head :: tail -> head @ (concatList tail)

  |[] -> []

// call the function

let primes = concatList listOfList

// print the results

printfn "%A" primes

 

下面是这个例子的结果:

 

[2; 3; 5; 7; 11; 13; 17; 19; 23; 29]

 

从列表中取出头进行处理,然后递归处理列表的尾,这是用模式匹配处理列表最通常的方法,但它并不当然是用模式匹配和列表所能做的唯一事情,下面的示例是这个组合的不同用法。

 

// function that attempts to find varioussequences

let rec findSequence l =

 match l with

  //match a list containing exactly 3 numbers

  |[x; y; z] ->

    printfn"Last 3 numbers in the list were %i %i %i"

     x y z

  //match a list of 1, 2, 3 in a row

  | 1:: 2 :: 3 :: tail ->

    printfn"Found sequence 1, 2, 3 within the list"

    findSequencetail

  //if neither case matches and items remain

  //recursively call the function

  |head :: tail -> findSequence tail

  //if no items remain terminate

  |[] -> ()

// some test data

let testSequence = [1; 2; 3; 4; 5; 6; 7; 8;9; 8; 7; 6; 5; 4; 3; 2; 1]

// call the function

findSequence testSequence

 

第一个规则演示如何匹配列表的固定长度,这里,是匹配列表中的三项,标识符用于提取这些项,因此,可以打印到控制台;第二个规则是看列表中的前三项是否是整数序列 1, 2, 3,如果是,就打印一个消息到控制台;最后两个规则是处理列表标准的头/尾方法,用于继续处理整个列表,如果没有前两个规则,什么也不做。

下面是运行的结果:

 

Found sequence 1, 2, 3 within the list

Last 3 numbers in the list were 3 2 1

 

虽然模式处理是分析列表数据的强大工具,但是,通常并不需要直接使用。F# 提供了大量的高阶函数(higher-order functions),能够处理列表,实现模式匹配,因此,我们并不需要写重复的代码了。为了说明问题,假设我们准备写一个函数,把列表中每一项加 1,可以很容易用模式匹配写出代码:

 

let rec addOneAll list =

 match list with

  |head :: rest ->

   head + 1 :: addOneAll rest

  |[] -> []

printfn "(addOneAll [1; 2; 3]) =%A" (addOneAll [1; 2; 3])

 

下面是运行的结果:

 

(addOneAll [1; 2; 3]) = [2; 3; 4]

 

然而,代码与这个简单的问题相比,显得有点啰嗦了。为列表中的每一项加1 只是更为普通的问题中的一个特例:即,为列表中的每一项应用一些转换。F# 的核心库有一个函数 map,它定义在 List 模块中,下面是它的定义:

 

let rec map func list =

 match list with

  |head :: rest ->

   func head :: map func rest

  |[] -> []

 

可以看到,map 函数的结构与前面示例中函数 addOneAll 很相似。如果列表不为空,取出列表中的头,并应用函数 func,由参数给定,然后把它追加到一个结果中,即针对列表中其余项递归地调用 map 函数;如果列表为空,就返回空列表。那么,map 函数就能够以更加简洁的方法实现为列表中所有项加 1 有功能:

 

let result = List.map ((+) 1) [1; 2; 3]

printfn "List.map ((+) 1) [1; 2; 3] =%A" result

 

运行结果如下:

 

(List.map ((+) 1) [1; 2; 3]) = [2; 3; 4]

 

还有一点要注意,示例中的加号是用括号括起来的函数,我们在本章前面的“运算符”一节讨论过。然后,这个函数由传递给它的第一个参数散应用,不是第二个参数;它会创建一个函数,取一个整数,返回一个整数;这个函数再传递给 map 函数。

List 模块中还有许多处理列表的函数,比如,List.filter和 List.fold,到第七章介绍 F# 库函数时再详细讨论。

 

 

列表推导(List Comprehensions)

 

列表推导可以方便地创建、转换集合。直接用推导语法可以创建列表、序列和数组,在下一章我们会详细讨论数组。序列(Sequences)是seq 类型的集合,它是.NET BCL IEnumerable类在 F# 中的名字,我们会在这一章“延迟计算”一节讲述。

最简单的推导是指定范围,给出第一项,可以是数字,也可以是字母,后面是两个点(..),然后是最后一项,所有的这些放在中括号([])内(创建列表),或者放在大括号({})内(创建序列)。编译器会计算出集合中的所有项,从第一个数开始,每次加 1,字符也类似,直到指定的最后项。下面的例子创建一个0 到9 的数字列表,一个A 到Z 的字符序列。

 

// create some list comprehensions

let numericList = [ 0 .. 9 ]

let alpherSeq = seq { 'A' .. 'Z' }

// print them

printfn "%A" numericList

printfn "%A" alpherSeq

 

运行结果如下:

 

[0; 1; 2; 3; 4; 5; 6; 7; 8; 9]

seq ['A'; 'B'; 'C'; 'D'; ...]

 

创建更有趣的集合是可以指定步长(注意,字符不支持这种类型的列表推断)。步长放在起止项之间,用(..)隔开。下面的例子创建两个列表,一个列表包含0 到30  之间 3 的倍数,另一个是从 9 递减到 0:

 

// create some list comprehensions

let multiplesOfThree = [ 0 .. 3 .. 30 ]

let revNumericSeq = [ 9 .. -1 .. 0 ]

// print them

printfn "%A" multiplesOfThree

printfn "%A" revNumericSeq

 

运行结果如下:

 

[0; 3; 6; 9; 12; 15; 18; 21; 24; 27; 30]

[9; 8; 7; 6; 5; 4; 3; 2; 1; 0]

 

列表推导也可以循环操作,从一个集合得到另一个集合。其思想是,枚举原来的集合,转换其中的每一项,把产生的项放到新的集合中。描述这样的循环,在列表推断的开始,使用关键字 for,后面是标识符,再加上关键字 in。下面的例子创建1 到10 之间整数平方的序列。

 

// a sequence of squares

let squares =

  seq{ for x in 1 .. 10 -> x * x }

// print the sequence

printfn "%A" squares

 

示例用 for 枚举集合 1..10,依次把每一项指定给标识符 x,然后,用 x 去计算新项,即,x 的平方。运行结果如下:

 

seq [1; 4; 9; 16; ...]

 

当定义列表推导,使用 F# 的关键字 yield 能够提供更多的灵活性,用 yield 可以决定是否把特定项加到集合中,看一下这样例子:

 

// a sequence of even numbers

let evens n =

  seq{ for x in 1 .. n do

      if x % 2 = 0 then yield x }

// print the sequence

printfn "%A" (evens 10)

 

示例的目的是创建偶数的集合,因此,要测试集合的中每一个枚举的数,看它是否是 2 的倍数,如果是,就用关键字 yield 返回,否则,就什么也不做。运行结果如下:

 

seq [2; 4; 6; 8; ...]

 

还有一种可能,在两维、多维上迭代进行列表推导,且每一维单独循环。下面的例子定义函数squarePoints,创建一个点的序列,构成方网,每个点用两个整数的元组表示。

 

// sequence of tuples representing points

let squarePoints n =

  seq{ for x in 1 .. n do

       for y in 1 .. n do

         yield x, y }

// print the sequence

printfn "%A" (squarePoints 3)

 

运行结果如下:

 

seq [(1, 1); (1, 2); (1, 3); (2, 1); ...]

 

在第四章中将讨论.NET 框架BCL 中的数组和集合的推导。

 

 

类型与类型推断(Types and Type Inference)

 

F# 是强类型语言,就是说,不能使用与函数不相符的值。如果函数的参数为字符串,不能用整型参数,必须进行显式转换。语言处理值的类型的方法称为类型系统(type system)。F# 的类型系统不同于常规编程,在F# 中,所有的值都有类型,值包括函数。

通常,不需要显式声明类型,编译器可推断出值的类型,根据函数中文字的类型以及它调用的其他函数结果的类型。如果一切正常,编译器就保留这个类型;如果类型不匹配,编译器就会报错,这个过程通常称为类型推断。如果想知道程序中类型的更多信息,可以加-i 开关,让编译器显示所有的推断类型。在Visual Studio 中,当把鼠标指向一个标识符时,工具提示会显示其类型。

F# 中类型推断的原理很容易理解。编译器进行完整扫描程序,确定定义的标识符类型,从开始的最左边标识符开始,一直到结束的最右边为止,根据它推断的类型去分配,即,文字的类型(更一般地讲),在其他源文件或程序集中定义函数的类型。

下面的例子定义两个F# 标识符,然后,用编译器的 -i 开关,在控制台上显示它们的推断类型。

 

let aString = "Spring time inParis"

let anInt = 42

 

val aString : string

val anInt : int

 

不出意外,这两个标识符的类型分别为字符串和整型,编译器描述它们的语法也直截了当,关键字val(即value,值),然后,是标识符,冒号,最后是类型。

下一个例子定义了函数makeMessage,更有趣。

 

let makeMessage x = (string_of_int x) +" days to spring time"

let half x = x / 2

 

val makeMessage : int -> string

val half : int -> int

 

注意,函数makeMessage 的定义以关键字val 作前缀,与前面的两个值一样,即使是函数,F# 编译器仍然把它看成值;另外,它的类型使用符号int -> string,表示函数接受整型,返回字符串。两个类型名字之间的->(是 ASCII 箭头,也可简称箭头),表示函数应用的转换。注意,->表示值的转换,但不一定是类型转换,因为,可以表示函数把一个值转换成同类型的另一个值,就像示例中第二行的half 函数:

函数类型能够散应用的,与参数为元组的函数是不同的,下面的函数div1、div2 就说明这一点。

 

let div1 x y = x / y

let div2 (x, y) = x / y

let divRemainder x y = x / y, x % y

 

val div1 : int -> int -> int

val div2 : int * int -> int

val divRemainder : int -> int -> int* int

 

 

函数 div1 是散应用,它的类型为int->int->int,表示参数可以一个一个地单独传递。与函数 div2相比,它的类型是int*int->int,它表示函数需要一对整数,即一个整数元组,然后,转换为一个整数。可以看到,函数div_remainder 执行整数除法,同时返回余数,它的类型是int->int->int*int,表示散函数后返回一个整数元组。

参见本章前面的“变量与函数”一节有关不同类型函数差异的解释。

下面的这个函数doNothing,看起来很不起眼,但从类型的视点看,却相当有趣。

 

let doNothing x = x

 

val doNothing : 'a -> 'a

 

这个函数的类型为'a -> 'a,表示函数接受一个类型值,返回一个类型相同的值。以单引号(')开始的任意类型表示可变类型(variable type)。F# 有一个类型obj,对应System.Object,表示任意类型的值。你也可能从其他通用语言运行时(common language runtime,CLR)熟悉这一概念(当然,许多语言并没有CLR)。然而,可变类型并不相同。注意,-> 两边都有类型 'a。这样,编译器在不知道类型的情况下,也可以知道返回值的类型与参数的类型相同。类型系统的这个功能,有时也称作类型参数化(type parameterization),可以让编译器在编译时找出更多的类型错误,并有帮于以避免类型转换(casting)。

 

注意:

可变类型,或类型参数化的概念,和泛型(Generics)的概念密切相关,它是在CLR 2.0 中引入的,现在已经成为CLI 2.0 的EMCA 规范的一部分。当F# 编译启用泛型的CLI 时,它可以完全利用泛型,并在任何地方使用,以找出未确定的类型。关注Don Syme 是值得的,它是F# 的创始人,在开始F# 之前,就设计并实现在.NET CLR 中的泛型。人们可以这样推测,正因为做了泛型,才有了F#。

 

下面的例子,函数doNothingToAnInt,演示了值被约束,叫类型约束(type constraint)。这里,函数的参数x 被强制为整型。尽管约束参数更常见,但是,约束任何标识符为特定的类型都是可能的,不仅是函数的参数。

看一下是如何限定一个标识符,列表stringList 并不是函数的参数。

 

let doNothingToAnInt (x : int) = x

let intList = [1; 2; 3]

 

let (stringList : list<string>) =["one"; "two"; "three"]

 

val doNothingToAnInt _int : int ->intval intList : int list

val stringList : string list

 

约束值为特定类型的语法很简单,在括号内,先是标识符的名字,加上冒号(:),再加上类型名。它有时也称为类型注释(type annotation)。

intList的值是整数列表,标识符的类型是intlist,这说明编译器已经确认这个列表只包含整数,这里,列表项的类型已确定是整型,任何企图向列表中添加非整型值都会引起编译错误。

标识符stringList 有类型注释,尽管这并不需要,因为编译器可以通过列表项的值判断其类型,这种语法更多地用于处理不确定类型时。把类型放在尖括号中间,在和它关联的类型后面,而不是仅仅把它写在类型名字前面。注意,虽然stringList 的类型被约束为list<string>(字符串列表),但是,当显示类型时,编译器仍然报告它的类型为 string list,就是说,它们完全是一回事。这个语法,使有类型参数的F# 类型看起来像其他.NET 库函数的泛型。

当写纯F# 时,通常不必要约束值,只是偶尔会用到,更多的在使用非F# 语言写的.NET库函数,和其他非托管库函数互操作时,在这两种情况下,编译器没有足够的类型信息,因此,通常需要给出足够的信息,以消除歧义。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值