函数式编程其实就是按照数学上的函数运算思想来实现计算机上的运算。虽然咱们不须要深刻了解数学函数的知识,但应该清楚函数式编程的基础是来自于数学。html
例如数学函数f(x) = x^2+x
,并无指定返回值的类型,在数学函数中并不须要关心数值类型和返回值。F#代码为let f x = x ** 2.0 + x
,F#代码和数学函数很是相似,其实这就是函数式编程的思想:只考虑用什么进行计算以及计算的结果(或者叫“输入和输出”),并不考虑怎样计算。正则表达式
其实,你能够把任何程序当作是一系列函数,输入是你鼠标和键盘的操做,输出是程序的运行结果。你不须要关心程序是怎样运行的,这些函数会根据你的输入来输出结果,而其中的算法是以函数的形式,而不是类或者对象。算法
下面咱们就先了解一些函数式编程中函数相关的东西。编程
在一个函数中改变了程序的状态(好比在文件中写入数据或者在内存中改变了全局变量)咱们称为反作用。像咱们使用printfn
函数,不管输入是什么,返回值均为unit
,但它的反作用是打印文字到屏幕上了。api
反作用并不必定很差,但却常常是不少bug的根源。咱们分别用命令式和函数式求一组数字的平方和:数组
let square x = x * x let sum1 nums = let mutable total = 0 for i in nums do let x = square i total <- total + x total let sum2 nums = Seq.sum (Seq.map square nums)
在sum2
中使用了Seq模块中的函数,这些函数将在稍候进行介绍。闭包
能够看出,函数式代码简短了许多,且少了不少变量的声明。并且sum1
是顺序执行,若想以并行方式运行则须要更改全部代码,但sum2
只须要替换其中的Seq.sum
和Seq.map
函数。app
在咱们接触到的非函数式编程语言(包括C#)中,函数和数值老是有一些不一样。但在函数式编程语言中,函数也是值。好比,函数能够做为其余函数的参数,也能够做为返回值(即高阶函数)。而这在函数式编程中是很是常见的。
须要注意的是,咱们叫“值”而不叫“变量”。由于在函数式编程中声明的东西默认是不可变的。(在F#中不彻底如此,是由于F#包含了面向对象编程范式,能够说并不是纯函数式编程语言。)编程语言
咱们看下面以函数做为参数的代码(求一组数字的负值):ide
> let negate x = -x;; val negate : x:int -> int > List.map negate [1..5];; val it : int list = [-1; -2; -3; -4; -5]
咱们使用函数negate
和列表[1..5]
做为List.map
的参数。
但不少时候咱们不须要给函数一个名称,只需使用匿名函数或叫Lambda表达式。在F#中,Lambda表达式为:关键字fun
和参数,加上箭头->
和函数体。则上面的代码能够更改成:
List.map (fun i-> -i) [1..5];;
咱们再看以函数做为返回值的例子,假设咱们定义一个powOf
函数,输入一个值,返回一个该值求幂的函数:
let powOf baseValue = (fun exp -> baseValue ** exp) let powOf2 = powOf 2.0 // f(x) = 2^x let powOf3 = powOf 3.0 // f(x) = 3^x powOf2 8. // 256.0 powOf3 8. // 6561.0
其中powOf2
即为powOf
函数使用参数2
返回的函数。其实这里涉及到闭包的内容,就不详细解释了,咱们详细函数式编程时可能会再说起。
递归你们都熟悉,只是在F#中声明时,须要添加rec
关键字:
let rec fact x = if x <= 1 then 1 else x * fact (x-1) fact 5;; (* val fact : x:int -> int val it : int = 120 *)
其实须要显示声明递归是由于F#的类型推断系统没法在函数声明完成以前肯定其类型,而使用rec
关键字后,就容许在肯定类型前调用该函数。
在函数式编程中,还有一个叫Partial Function(暂且叫部分函数吧)的,能够把接收多个参数的函数分解成接收单个参数,即柯里化(Currying)。
咱们知道,使用函数printfn
打印整数的语句为printfn "%d" i
,咱们定义一个打印整数的函数:
> let printInt i = printfn "%d" i;; val printInt : i:int -> unit > let printInt = printfn "%d";; val printInt : (int -> unit)
在F#中,如+ - * /
等运算符其实属于内建函数。而咱们也可使用这些符号来自定义符号函数。
咱们用符号来从新定义上面的阶乘函数:
let rec (!) x = if x <= 1 then 1 else x * !(x - 1) !5;; (* val ( ! ) : int -> int val it : int = 120 *)
须要注意的是,符号函数通常须要括号包裹,若是符号函数的参数不止一个,则符号函数是以中缀的方式来使用,例如咱们用=~=
定义一个验证字符串是否和正则表达式匹配的函数:
open System.Text.RegularExpressions;; let (=~=) str (regex : string) = Regex.Match(str, regex).Success "The quick brown fox" =~= "The (.*) fox";; (* val ( =~= ) : string -> string -> bool val it : bool = true *)
并且,符号函数也能够做为高阶函数的参数。
|>
和<|
咱们再返回来看上面的平方和函数:
let sum2 nums = Seq.sum (Seq.map square nums)
假如函数层次很是多,一层包裹一层,则可读性很是差。
在F#定义了以下符号函数
let (|>) x f = f x let (<|) f x = f x
咱们称为“正向管道符”和“逆向管道符”。则上面的平方和函数可写做:
let sum2 nums = nums |> Seq.map square |> Seq.sum
<|
虽然用得很少,但经常使用来改变优先级而无需使用括号:
let sum2 nums = Seq.sum <| Seq.map square nums
>>
和<<
咱们也能够用函数合成符将多个函数组合成一个函数,合成符也分正向(>>
)和逆向(<<
)。
let (>>) f g x = g(f x) let (<<) f g x = f(g x)
仍是以上面的求平方和为例(Seq.map square
便是一个部分函数):
let sum2 nums = (Seq.map square >> Seq.sum) nums let sum2 nums = (Seq.sum << Seq.map square) nums
在上一篇中,咱们了解了集合类型。在F#中,为这些集合类型定义了许多函数,分别在集合名称对应的模块中,例如Seq的相关函数位于模块Microsoft.FSharp.Collections.Seq
中。而这也是咱们最经常使用到的模块。
**模块(module
)**是F#中组织代码的一种方式,相似于命令空间(namespace
)。但F#中也是有命名空间的,其间的区别将在下一篇介绍。
下面简单介绍经常使用的函数,并会列出与.Net的System.Linq
中对应的函数。
如无特别说明,该函数在三个模块中都可用,但由于集合的实现方式不一样,函数的复杂度也会有区别,在使用中根据实际状况选择合适的函数。
对应于Linq中的Count
。即得到集合中元素的个数。
[1..10] |> List.length;; // 10 Seq.length {1..100};; // 100
虽然在Seq
中也有length
函数,但谨慎使用,由于Seq可能为无限序列。
exists
用于判断集合是否存在符合给定条件的元素,对应于Linq中的Any
。而exists2
用于判断两个集合是否包含在同一位置且符合给定条件的一对元素。
List.exists ((=) 3) [1;3;5;7];; //true Seq.exists (fun n1 n2 -> n1=n2) {1..5} {5..-1..1};; //true
第一行代码判断列表中是否包含等于3的元素,其中(=) 3
即为部分函数,注意=
为符号函数。
第二行代码判断两个序列中,由于{1;2;3;4;5}
和{5;4;3;2;1}
在索引2的位置存在元素符合函数(fun n1 n2 -> n1=n2)
,因此返回true
。
forall
检查是否集合中全部元素均知足指定条件,对应Linq中的All
。
let nums = {2..2..10} nums |> Seq.forall (fun n -> n % 2 = 0);; //true
而forall2
和exists2
相似,但当且仅当全部元素都知足相同位置且符合给定条件才返回true
。接上一个代码片断:
let nums2 = {12..2..20} Seq.forall2 (fun n n2 -> n + 10 = n2) nums nums2;; //true
find
查找符合条件的第一个元素,对应Linq中的First
。须要注意的是当不存在符合条件的元素,将引起KeyNotFoundException
异常。
Seq.find (fun i -> i % 5 = 0) {1..100};; //5
findIndex
则返回符合条件的第一个元素的索引。
map
对应Linq中的Select
,将函数应用于集合中的每一个元素,返回值产生一个新的集合。
List.map ((*) 2) [1..10];; // [2; 4; 6; 8; 10; 12; 14; 16; 18; 20]
mapi
与map
相似,不过在应用的函数中还须要传入一个整数做为集合的索引。
Seq.mapi(fun i x -> x*i) [3;5;7;8;0];; // 将各个元素乘以各自的索引,结果为:[0; 5; 14; 24; 0]
iter
将函数应用于集合中的每一个元素,但函数返回值为unit。功能相似于for
循环。 而iteri
与mapi
同样须要在函数中传入一个索引。
Seq.iteri(fun i x -> printfn "第%d个元素为:%d" i x) [3;5;7;8;0] (* 第0个元素为:3 第1个元素为:5 …… *)
F#中filter
和where
是同样的,对应于Linq中的Where
。用于查找符合条件的元素。
{1..10} |> Seq.filter (fun n -> n%2 = 0);; //val it : seq<int> = seq [2; 4; 6; 8; ...]
fold
对应Linq中的Aggregate
,经过提供初始值,而后将函数逐个应用于每一个元素,返回单一值。
Seq.fold (fun acc n -> acc + n) 0 {1..5};; //15 Seq.fold (fun acc n -> acc + string n) "" {1..10};; //"12345"
首先,将初始值与第一个元素应用于函数,再将返回值与第二个元素应用于函数,依此类推……
Linq中的Aggregate
包含不须要提供初始值的重载,其实F#中也有对应的reduce
函数。相似的还有foldBack
和reduceBack
等逆向操做,这里就不介绍了。
collect
对应Linq中的SelectMany
,展开集合并返回全部二级集合的元素。
let lists = [ [0;1]; [0;1;2]; [0;1;2;3] ] lists |> List.collect id;; //[0; 1; 0; 1; 2; 0; 1; 2; 3]
其中id为Operators
模块中的函数,它的实现为fun n->n
,即直接对参数进行返回。
append
将两个集合类型合并成一个,对应于Linq中的Concat
。
> Array.append [|1;3;1;4|] [|5;2;0|];; val it : int [] = [|1; 3; 1; 4; 5; 2; 0|]
zip
函数将两个集合合并到一个里,合并后每一个元素是一个二元元组。
let list1 = [ 1..3 ] let list2 = [ "a";"b";"c" ] List.zip list1 list2;; // [(1, "a"); (2, "b"); (3, "c")]
zip3
顾名思义,就是将三个集合合并到一个里。
合并后的长度取决于最短的集合的长度。
rev
函数反转一个列表或数组,在Seq模块中没有这个函数。
sort
函数基于compare函数(第二篇中的“比较”介绍过)对集合中的元素进行排序。
> List.sort [1;3;-2;2];; val it : int list = [-2; 1; 2; 3]
Linq中包含Max
、Min
、Average
和Sum
等数学函数。F#集合模块中也有对应的函数。
List.max [1..10] //10 Seq.min {1..5} //5 [1..10] |> List.map float |> List.average //5.5 List.averageBy float [1..10] //5.5 [0..100] |> Seq.where (fun x -> x % 2 <> 0) |> Seq.sum |> printf "0到100中的奇数的和为%i" // 0到100中的奇数的和为2500
须要注意的是,average
函数须要集合中的元素支持精确除法(Exact division,即实现了DivideByInt
函数的类型。不知道为何是ByInt。),而F#中又不支持隐式类型转换,因此对int
集合求平均值只能先转换为float
或float32
,或使用averageBy
函数。
sum
函数的示例代码将第一篇中由C#翻译过来的命令示示例代码转换成了函数式的代码。
三种集合类型的对应模块中,均提供转换**到(to)另外两种集合类型,和从(of)**另外两种类型转换的函数。
如Seq模块,经过Seq.toList
和Seq.toArray
函数转出;经过Seq.ofList
和Seq.ofArray
转入。
Seq.toList {1..5};; //[1; 2; 3; 4; 5] List.ofArray [|1..5|];; //[1; 2; 3; 4; 5]
函数式编程,核心就是函数的运用。上面介绍的这些在C#中也常用到对应的方法,但F#提供的函数很是丰富,你们可经过MSDN了解更多:
由于F#中的List和Array均实现了IEnumarable<T>
接口,因此Seq模块的函数也能够接收List类型和Array类型的参数。固然,反之则不行。
到如今为止,咱们了解的F#都是在交互窗口中。下一篇咱们再简单介绍项目建立和代码组织,即模块相关。
本文发表于博客园。 原文连接为:http://www.cnblogs.com/hjklin/p/fs-for-cs-dev-4.html。
可前往博客园查看更多文章。