如果你也会C#,那不妨了解下F#(4):了解函数及常用函数
函数式编程其实就是按照数学上的函数运算思想来实现计算机上的运算。虽然我们不需要深入了解数学函数的知识,但应该清楚函数式编程的基础是来自于数学。
例如数学函数\(f(x) = x^2+x\),并没有指定返回值的类型,在数学函数中并不需要关心数值类型和返回值。F#代码为let f x = x ** 2.0 + x,F#代码和数学函数非常类似,其实这就是函数式编程的思想:只考虑用什么进行计算以及计算的结果(或者叫“输入和输出”),并不考虑怎样计算。
其实,你可以把任何程序看成是一系列函数,输入是你鼠标和键盘的操作,输出是程序的运行结果。你不需要关心程序是怎样运行的,这些函数会根据你的输入来输出结果,而其中的算法是以函数的形式,而不是类或者对象。
下面我们就先了解一些函数式编程中函数相关的东西。
了解函数
不可变性
在一个函数中改变了程序的状态(比如在文件中写入数据或者在内存中改变了全局变量)我们称为副作用。像我们使用printfn函数,无论输入是什么,返回值均为unit,但它的副作用是打印文字到屏幕上了。
副作用并不一定不好,但却经常是很多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函数。
函数和值
在我们接触到的非函数式编程语言(包括C#)中,函数和数值总是有一些不同。但在函数式编程语言中,函数也是值。比如,函数可以作为其他函数的参数,也可以作为返回值(即高阶函数)。而这在函数式编程中是非常常见的。
需要注意的是,我们叫“值”而不叫“变量”。因为在函数式编程中声明的东西默认是不可变的。(在F#中不完全如此,是因为F#包含了面向对象编程范式,可以说并非纯函数式编程语言。)
我们看下面以函数作为参数的代码(求一组数字的负值):
> 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
在函数式编程中,还有一个叫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中对应的函数。
如无特别说明,该函数在三个模块中均可用,但因为集合的实现方式不同,函数的复杂度也会有区别,在使用中根据实际情况选择合适的函数。
length
对应于Linq中的Count。即获得集合中元素的个数。
[1..10] |> List.length;; // 10
Seq.length {1..100};; // 100
虽然在Seq中也有length函数,但谨慎使用,因为Seq可能为无限序列。
exists 和 exists2
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 和 forall2
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 和 findIndex
find查找符合条件的第一个元素,对应Linq中的First。需要注意的是当不存在符合条件的元素,将引发KeyNotFoundException异常。
Seq.find (fun i -> i % 5 = 0) {1..100};; //5
findIndex则返回符合条件的第一个元素的索引。
map 和 mapi
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 和 iteri
iter将函数应用于集合中的每个元素,但函数返回值为unit。功能类似于for循环。
而iteri与mapi一样需要在函数中传入一个索引。
Seq.iteri(fun i x -> printfn "第%d个元素为:%d" i x) [3;5;7;8;0]
(*
第0个元素为:3
第1个元素为:5
……
*)
filter 和 where
F#中filter和where是一样的,对应于Linq中的Where。用于查找符合条件的元素。
{1..10} |> Seq.filter (fun n -> n%2 = 0);;
//val it : seq<int> = seq [2; 4; 6; 8; ...]
fold
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
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
append将两个集合类型合并成一个,对应于Linq中的Concat。
> Array.append [|1;3;1;4|] [|5;2;0|];;
val it : int [] = [|1; 3; 1; 4; 5; 2; 0|]
zip 和 zip3
zip函数将两个集合合并到一个里,合并后每个元素是一个二元元组。
let list1 = [ 1..3 ]
let list2 = [ "a";"b";"c" ]
List.zip list1 list2;;
// [(1, "a"); (2, "b"); (3, "c")]
zip3顾名思义,就是将三个集合合并到一个里。
合并后的长度取决于最短的集合的长度。
rev
rev函数反转一个列表或数组,在Seq模块中没有这个函数。
sort
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。
如果你也会C#,那不妨了解下F#(4):了解函数及常用函数的更多相关文章
- 如果你也会C#,那不妨了解下F#(7):面向对象编程之继承、接口和泛型
前言 面向对象三大基本特性:封装.继承.多态.上一篇中介绍了类的定义,下面就了解下F#中继承和多态的使用吧.
- 如果你也会C#,那不妨了解下F#(5):模块、与C#互相调用
F# 项目 在之前的几篇文章介绍的代码都在交互窗口(fsi.exe)里运行,但平常开发的软件程序可能含有大类类型和函数定义,代码不可能都在一个文件里.下面我们来看VS里提供的F#项目模板. F#项目模 ...
- 如果你也会C#,那不妨了解下F#(6):面向对象编程之“类”
前言 面向对象的思想已经非常成熟,而使用C#的程序员对面向对象也是非常熟悉,所以我就不对面向对象进行介绍了,在这篇文章中将只会介绍面向对象在F#中的使用. F#是支持面向对象的函数式编程语言,所以你用 ...
- 如果你也会C#,那不妨了解下F#(3):F#集合类型和其他核心类型
本文链接:http://www.cnblogs.com/hjklin/p/fs-for-cs-dev-3.html 在第一篇中,我们介绍了一些基础数据类型,其实那篇标题中不应该含有"F#&q ...
- 如果你也会C#,那不妨了解下F#(1):F# 数据类型
本文链接:http://www.cnblogs.com/hjklin/p/fs-for-cs-dev-1.html 简单介绍 F#(与C#一样,念作"F Sharp")是一种基于. ...
- 如果你也会C#,那不妨了解下F#(2):数值运算和流程控制语法
本文链接:http://www.cnblogs.com/hjklin/p/fs-for-cs-dev-2.html 一些废话 一门语言火不火,与语言本身并没太大关系,主要看语言的推广. 推广得好,用的 ...
- linux下实现在程序运行时的函数替换(热补丁)
声明:以下的代码成果,是参考了网上的injso技术,在本文的最后会给出地址,同时非常感谢injso技术原作者的分享. 但是injso文章中的代码存在一些问题,所以后面出现的代码是经过作者修改和检测的. ...
- 在 mongodb 终端环境下写多行 javascript 代码、函数
工作中碰到一个问题,需要把某个 collection 中的某些符合条件的数据取出来,逐行处理其中某些字段.mongodb 终端下支持直接写 js 代码.函数,也可以运行 js 文件.1 首先需要设置 ...
- java下实现调用oracle的存储过程和函数
在Oracle下创建一个test的账户,然后 1.创建表:STOCK_PRICES --创建表格 CREATE TABLE STOCK_PRICES( RIC VARCHAR() PRIMARY KE ...
随机推荐
- Partition:分区切换(Switch)
在SQL Server中,对超级大表做数据归档,使用select和delete命令是十分耗费CPU时间和Disk空间的,SQL Server必须记录相应数量的事务日志,而使用switch操作归档分区表 ...
- 【Win 10 应用开发】在App所在的进程中执行后台任务
在以往版本中,后台任务都是以独立的专用进程来运行,因此,定义后台任务代码的类型都要位于 Windows 运行时组件项目中. 不过,在14393中,SDK 作了相应的扩展,不仅支持以前的独立进程中运行后 ...
- ASP.NET MVC5+EF6+EasyUI 后台管理系统(66)-MVC WebApi 用户验证 (2)
系列目录 前言: 回顾上一节,我们利用webapi简单的登录并进行了同域访问与跨域访问来获得Token,您可以跳转到上一节下载代码来一起动手. 继续上一篇的文章,我们接下来演示利用拿到的Token来访 ...
- Phoenix综述(史上最全Phoenix中文文档)
个人主页:http://www.linbingdong.com 简书地址:http://www.jianshu.com/users/6cb45a00b49c/latest_articles 网上关于P ...
- 关于VS2015 ASP.NET MVC添加控制器的时候报错
调试环境:VS2015 数据库Mysql WIN10 在调试过程中出现类似下两图的同学们,注意啦. 其实也是在学习的过程中遇到这个问题的,找了很多资料都没有正面的解决添加控制器的时候报错的问题,还是 ...
- VS15 preview 5打开文件夹自动生成slnx.VC.db SQLite库疑惑?求解答
用VS15 preview 5打开文件夹(详情查看博客http://www.cnblogs.com/zsy/p/5962242.html中配置),文件夹下多一个slnx.VC.db文件,如下图: 本文 ...
- Linux主机上使用交叉编译移植u-boot到树莓派
0环境 Linux主机OS:Ubuntu14.04 64位,运行在wmware workstation 10虚拟机 树莓派版本:raspberry pi 2 B型. 树莓派OS: Debian Jes ...
- 浅谈Java的throw与throws
转载:http://blog.csdn.net/luoweifu/article/details/10721543 我进行了一些加工,不是本人原创但比原博主要更完善~ 浅谈Java异常 以前虽然知道一 ...
- Atitit.技术管理者要不要自己做开发??
Atitit.技术管理者要不要自己做开发?? 1. 为什么很多管理者不能自己亲自做了1 1.1. 沟通成本多了1 1.2. .组织分散. 1 1.3. 会议多 .协调多 1 1.4. 问题的根源在于我 ...
- Princeton Algorithms week3 Assignment
这周编程作业是实现检测点共线的算法.和排序算法有关系的地方在于,对斜率排序后可以很快的检测出来哪些点是共线的,另外这个算法的瓶颈也在于排序的性能. 一点收获: java传参数时传递的是值,这很多人都知 ...