不变性
在F#语言中,不存在像其他语言如 C#中那样有"变量"这一说话,起而代之的是"值"。其原因是,在函数编程中,你定义的任何事物的名称在默认下是不可改变的,也就是说它们的值不能被改变,而我们知道,变量是可以随时被改变的。
如果一个函数改变了程序的某种状态,比如写入一个文件或者改变内存中的全局变量,就被称为"副作用"。比如调用printfn函数返回一个unit类型,但是这里有一个副作用,就是打印文本到屏幕上。同样的,如果一个函数改变了内存中的值,这也是一个副作用-在这个函数中做了一些额外的事情除了不返回值。
所用的副作用都不是那么的坏,但有些副作用都是意想不到的错误根源,即使是非常有经验的程序员,如果不清楚一个函数的副作用,也会烦错误的。值的不变性可以帮助你编写更安全的代码,因为你不能改变你不能改变的东西。
如果你使用的是命令式编程语言,如果让它不能有变量,看起来是一个不可能的事情,但是不变性却提供了重要的好处,例如下面的代码,两个函数简单的统计列表中每个元素的乘方之和。一个函数采用的是命令式编程风格,另一个是采用函数式编程风格。命令式编程风格中利用了一个mutable变量
,意思是total在函数imperativeSum.执行期间值一直是在变化的:
> let square x = x * x;;
val square : int -> int
> let imperativeSum numbers =
- let mutable total = 0
- for i in numbers do
- let x = square i
- total <- total + x
- total;;
val imperativeSum : seq<int> -> int
> let functionalSum numbers =
- numbers
- |> Seq.map square
- |> Seq.sum;;
val functionalSum : seq<int> -> int
> imperativeSum [1..2];;
val it : int = 5
> functionalSum [1..2];;
val it : int = 5
在上面两个函数中,输入同样的值,结果都是相同的。那么函数式编程风格的好处在哪里体现出来了呢?首先,你会注意到可读性方面。在命令式风格中,你必须通过阅读代码才能知道是怎么一回事。函数式风格,然而更加的简单明了,代码直接映射到你所想要发生的地方。同时,如果你想在命令式语言中并行运行代码,你将不得不完全重写代码。因为你是如此详细的指定了你的程序时如何运行的,如果你需要并行运行它,你就必须在并行中做好更详细的运行步骤。然而在函数式风格中,是没有规定如果做事情的,所以你可以在并行实现中很容易的交换map和sum。
函数值
在大部分其他编程语言中,函数和数据被认为是两个不同的事情。然而,在函数编程语言中,函数与其他数据一样,一视同仁。例如,函数可以作为一个另一个函数的参数。实际上,函数能够创建并且返回一个新的函数,函数能够返回其他的函数作为他们的输入或者输出,被称为高阶函数(higher-order functions),也是函数编程中的基本。
高阶函数可以使你在代码中使用抽象或重用。下面定义了一个negate函数,返回一个数的否定数。当该函数作为一个List.map的参数时,将被应用到整个列表,并且作用于每一个元素:
> let negate x = -x;;
val negate : int -> int
> List.map negate [1..10];;
val it : int list = [-1; -2; -3; -4; -5; -6; -7; -8; -9; -10]
使用函数值是非常方便的,但是,你写出来的很多简单的功能,没有很大的利用价值。比如函数negat可能就不会用到任何地方,除非你特意去做一个返回否定数的功能。在现实用,对于一些小函数作为一个另函数的参数,并不是都要事先定义好来的,在这里,我们可以用到另一种风格-匿名函数,也被称作为lambda表达式(lambda expression),这样就可以创建一个内联函数。
要创建一个lambda表达式,只需要使用fun关键字并且在其后面加上函数参数,随后加上一个箭头->就行了,下面的代码是利用lambda表达式改写的negat函数:
> List.map (fun x -> -x) [1..10];;
val it : int list = [-1; -2; -3; -4; -5; -6; -7; -8; -9; -10]
怎样,是不是觉得简单多了,这种风格在函数编程语言中是经常碰到的,特别是在Erlang语言中。
函数的部分应用(Partial function application)
函数的另一个非常有用的地方时函数的部分应用,也被称作为"currying"。让我们利用现有的.NET库来写一个简单的函数:在文本文件后面追加内容:
> open System.IO
- let appendFile (fileName:string) (text:string) =
- use file = new StreamWriter(fileName,true)
- file.WriteLine(text)
- file.Close();;
val appendFile : string -> string -> unit
> appendFile @"D:/Log.txt" "Processing Event X...";;
val it : unit = ()
该appendFile功能似乎很简单,但是如果你想重复写入相同的日志文件。你必须保持日志文件的路径总是作为函数的第一个参数,它看起来很好。但是,如果使用的频率很大,每次记录日志的时候都需要手动输入两个参数值,而且第一个还是不变的,这样就显示很僵化,并且如果以后更改路径也是一件麻烦的事情。在命令式编程语言中,或者可以采用方法重写,定义一个路径常量来解决。但是在F#中,有一个更好的方法,即部分函数应用。
部分函数应用是能够指定一个函数的一些参数,产生一个新的函数,并且指定的那些参数是固定的,上面我们的那个记录日志的函数就可以把路径参数指定为一个部分函数的固定值,这样,就只有一个参数可以让用户使用了:
> let curriedAppendFile = appendFile @"D:/Log.txt";;
val curriedAppendFile : (string -> unit)
> curriedAppendFile "Processing Event Y...";;
val it : unit = ()
返回函数的函数(Functions returning functions)
在函数编程中,在处理数据上,存在一种可能,就是函数可以返回函数作为它的返回值。下面的代码中定义了一个函数generatePowerOfFunc,它返回一个给定一个数的N次方的函数。同时还创建了powerOfTwo和powerOfThree函数:
> let generatePowerOfFunc num = (fun exponent -> num ** exponent);;
val generatePowerOfFunc : float -> float -> float
> let powerOfTwo = generatePowerOfFunc 2.0;;
val powerOfTwo : (float -> float)
> powerOfTwo 8.0;;
val it : float = 256.0
> let powerOfThree = generatePowerOfFunc 3.0;;
val powerOfThree : (float -> float)
> powerOfThree 2.0;;
val it : float = 9.0