F#程序设计-函数式编程之惰性加载

来源:互联网 发布:淘宝怎么添加菜鸟驿站 编辑:程序博客网 时间:2024/05/29 08:30

在日常软件开发中,大部分编写的代码进行一些计算、求值都是属于"热切"的,意思就是说,只要你执行一个计算,结果就会马上返回给你,不管你现在用不用得着。在F#中,当然也是这样的,然而,就像前面说的,有时你仅仅是预先定义执行的代码,然后等需要用到的时候再执行,获取值,这种情况,在F#中叫做惰性加载。

 

关于惰性加载,熟悉Hibernate的程序员应都很了解,它其中一个重要的地方,就是惰性加载,惰性加载在有的时候很有用处,它能够提高程序的性能,特别是从数据库中频繁操作数据的时候,同时惰性加载也能大幅度减少内存的使用,因为你仅仅是需要它的时候才在内存中创建值。在F#中,有两种类型提供惰性加载,一个是Lazy,另外一个是sequences(也就是seq<'a>类型)。

 

Lazy 类型

一个Lazy类型就是一个thunk或者是计算的占位符。一旦创建一个Lazy类型,你就可以随时在需要的地方使用它的惰性值,就好像它已经被求值。但是,如果多个地方使用它,它仅仅是被计算一次,当面临外界的"强迫"时。

下面的例子创建了x和y值,然后被打印到控制台。因为它们被定义成了Lazy类型,所以在定义的时候不会被执行。只有当y被外界强迫执行时,从而也对x进行求值,为了构造一个Lazy值,你需要使用一个关键字Lazy或者利用Lazy<>.Create()函数。

 

> let x = Lazy<int>.Create(fun () -> printfn "Evaluating x..."; 10)
- let y = lazy (printfn "Evaluating y..."; x.Value + x.Value);;

val x : System.Lazy<int> = Value is not created.
val y : Lazy<int> = Value is not created.

 


>(*第一次使用,执行了printfn*)

y.Value;;
Evaluating y...
Evaluating x...
val it : int = 20


>(*第二次使用,没有执行printfn*)

y.Value;;
val it : int = 20

 

Sequences

在F#中,虽然使用Lazy可以实现惰性加载,但是,最常见的还是通过sequence或者seq类型来实现的。sequence跟List一样,都是代表一个有序的元素集合,例如下面的代码段定义了一个由五个元素的序列,并且通过Seq.iter函数来遍历每个元素:

> let seqOfNumbers = seq {1..5};;

val seqOfNumbers : seq<int>

> seqOfNumbers |> Seq.iter (printfn "%d");;
1
2
3
4
5
val it : unit = ()

在上面的代码中,第一行定义了一个seq,它没有像定义List样,马上把元素值给计算出来了,而且在等到外界在需要使用的时候再计算。

在平常的使用中,Seq和List的使用方式是一样的,而且两个模块内置的函数的功能都是一样。

 

那么,你就会这样想,既然在使用方式以及实现的功能上都是一样的,为什么要存在两种这样的类型,而不仅仅使用List就行了呢。这是因为这两个类型的内部原理是不一样的,在List中,所有的值都存在于内存中,如果一个List的元素非常的巨大,那么,从内存上考虑,会把内存消耗尽得。而Seq就不会,它的值并没有在内存中停放,你可以定义一个无限的序列。而且,在List中,你必须提前知道每个元素的值,而在Seq中,是可以动态获取的。

 

比如下面的例子,各定义了一个0到Int32类型最大值的List和Seq。但是由于List的值是保存在内存中的,面对如此巨大的List,内存很快被耗尽完了,所以也就创建列表失败。但是Seq却没有问题:

> // Sequence of all integers
let allIntsSeq = seq { for i = 0 to System.Int32.MaxValue -> i };;
val allIntsSeq : seq<int>
> allIntsSeq;;
val it : seq<int> = seq [0; 1; 2; 3; ...]
> // List of all integers - ERROR: Can't fit in memory!
let allIntsList = [ for i = 0 to System.Int32.MaxValue -> i ];;
System.OutOfMemoryException: Exception of type 'System.OutOf MemoryException' was thrown.

 

Sequence表达式

简单的定义一个Seq,可以在Seq关键字后紧接着中括号,然后在中括号内用分号分隔符输入元素,例如:

> let seqNumbers = seq [1;2;3];;

val seqNumbers : seq<int> = [1; 2; 3]

 

同样也可以像List一样,通过表达式来定义Seq,这样的就,就在seq后面接着一对大括号,然后在大括号里面写表达式:

> let alphabet = seq {for c in 'A'..'Z' -> c};;

val alphabet : seq<char>

> Seq.take 4 alphabet;;
val it : seq<char> = seq ['A'; 'B'; 'C'; 'D']

 

在前面,我们提到过,Seq也是惰性加载的,所以每产生一个元素,Seq内的代码仍然在执行。也就是说,没Seq内的元素不是已经产生就停留在内中中的,对于动态求值的Seq,每次都是通过执行相应的代码来产生元素,下面的一个例子就很好的说明了这个,在下面的例子中,通过表达式来动态获取元素值,同时在控制台上打印出来:

> let noisyAlphabet =
-     seq
-         {
-             for c in 'A' .. 'Z' do
-                 printfn "Yielding %c..." c
-                 yield c
-         };;

val noisyAlphabet : seq<char>

> let fifthLetter = Seq.nth 4 noisyAlphabet;;
Yielding A...
Yielding B...
Yielding C...
Yielding D...
Yielding E...

val fifthLetter : char = 'E'

> let fifthLetter = Seq.nth 4 noisyAlphabet;;
Yielding A...
Yielding B...
Yielding C...
Yielding D...
Yielding E...

val fifthLetter : char = 'E'

 

同时我们拿一个List来对比,List的元素已经产生,就已经停留在内存中,所以第二次求值的时候,就不会经过执行代码来获取,而且直接从内存中获取:

> let listAlphabet =
-     [
-         for c in 'A' .. 'Z' do
-             printfn "Yielding %c..." c
-             yield c
-     ];;
Yielding A...
Yielding B...
Yielding C...
Yielding D...
Yielding E...
Yielding F...
Yielding G...
Yielding H...
Yielding I...
Yielding J...
Yielding K...
Yielding L...
Yielding M...
Yielding N...
Yielding O...
Yielding P...
Yielding Q...
Yielding R...
Yielding S...
Yielding T...
Yielding U...
Yielding V...
Yielding W...
Yielding X...
Yielding Y...
Yielding Z...

val listAlphabet : char list =
  ['A'; 'B'; 'C'; 'D'; 'E'; 'F'; 'G'; 'H'; 'I'; 'J'; 'K'; 'L'; 'M'; 'N'; 'O';
   'P'; 'Q'; 'R'; 'S'; 'T'; 'U'; 'V'; 'W'; 'X'; 'Y'; 'Z']

> listAlphabet;;
val it : char list =
  ['A'; 'B'; 'C'; 'D'; 'E'; 'F'; 'G'; 'H'; 'I'; 'J'; 'K'; 'L'; 'M'; 'N'; 'O';
   'P'; 'Q'; 'R'; 'S'; 'T'; 'U'; 'V'; 'W'; 'X'; 'Y'; 'Z']

这样,对比一下,现在可以发现Seq比起List来,是有多好了吧。

 

在Seq中,还有一个必要重要的方面是,可以利用yield!来实现递归。你可以利用yield!返回一个子Seq,返回的子Seq合并到主Seq中,比如下面的示例。函数allFilesUnder返回一个字符类型的Seq,它递归一个给定的路径内的的所有文件,Directory.GetFiles返回一个包含给定的路径下的所有文件的字符数组,因为数组与序列是兼容的,所以yield!返回所有的文件:

> open System.IO
- let rec allFilesUnder basePath =
-     seq {
-             yield! Directory.GetFiles(basePath)
-             for subdir in Directory.GetDirectories(basePath) do
-                 yield! allFilesUnder subdir
-          };;

val allFilesUnder : string -> seq<string>

 

> let allFiles =
- allFilesUnder @"E://Downloads//VMware-workstation-6.5.3-185404_XiaZAiBa";;

 

val allFiles : seq<string>

 

Seq模块函数

Seq模块内包含有一些比较常用的函数,比如Seq.take、Seq.unfold等。

Seq.take函数在前面的例子中已经见过了,它的作用就是返回序列前N个的元素。Seq.unfold的作用是根据一个给定的函数产生一个序列,它的类型是:

('a -> ('b * 'a) option) ->'a -> seq<'b>.

 

在Seq.unfold中,给定的函数提供一个输入值,并且这个函数返回Option类型,Option类型内包含一个元组,作为Seq.unfold下一次迭代时的值,如果函数返货None,则表达Seq已经结尾,下面用一个求斐波那契数列例子来直观的说明Seq.unfold的原理:

> let nextFibUnder100 (a,b) =
-     if  a + b > 100 then
-         None
-     else
-         let nextValue = a + b
-         Some(nextValue,(nextValue,a));;

val nextFibUnder100 : int * int -> (int * (int * int)) option

 

> let fibsUnder100 = Seq.unfold nextFibUnder100 (0, 1);;

val fibsUnder100 : seq<int>

 

> Seq.toList fibsUnder100;;
val it : int list = [1; 1; 2; 3; 5; 8; 13; 21; 34; 55; 89]

 

原创粉丝点击