函数式编程是种编程典范,它将电脑运算视为函数的计算。函数编程语言最重要的基础是 λ 演算(lambda calculus)。而且λ演算的函数可以接受函数当作输入(参数)和输出(返回值)。和指令式编程相比,函数式编程强调函数的计算比指令的执行重要。和过程化编程相比,函数式编程里,函数的计算可随时调用。
命令式编程是面向计算机硬件的抽象,有变量(对应着存储单元),赋值语句(获取,存储指令),表达式(内存引用和算术运算)和控制语句(跳转指令),一句话,命令式程序就是一个冯诺依曼机的指令序列。
而函数式编程是面向数学的抽象,将计算描述为一种表达式求值,一句话,函数式程序就是一个表达式。
面向对象:
1.数据和对数据的操作紧紧耦合
2. 对象隐藏它们操作的实现细节,其他对象调用这些操作只需要通过接口。
3. 核心抽象模型是数据自己
4. 核心活动是组合新对象和拓展已经存在的对象,这是通过加入新的方法实现的。
函数编程:
- 数据与函数是松耦合的
- 函数隐藏了它们的实现,语言的抽象是函数,以及将函数组合起来表达。
- 核心抽象模型是函数,不是数据结构
- 核心活动是编写新的函数。
- 变量缺省是不变的,减少可变性变量的使用,并发性好
Scala是一种成熟的函数式语言。但,Scala不强迫使用函数式的风格,必要情况下,可以写成指令形式,用可变数据或有副作用的方法调用。但是Scala有更好的函数式编程方式做替代,因此通常可以轻松地避免使用它们。
函数式编程中的函数这个术语不是指计算机中的函数(实际上是Subroutine),而是指数学中的函数,即自变量的映射。也就是说一个函数的值仅决定于函数参数的值,不依赖其他状态。比如sqrt(x)函数计算x的平方根,只要x不变,不论什么时候调用,调用几次,值都是不变的。
函数编程的一些基本特点包括:
支持闭包和高阶函数,支持惰性计算(lazy evaluation)。使用递归作为控制流程的机制。加强了引用透明性。没有副作用。
函数式编程有两种指导理念。
函数是”第一等公民”。
所谓”第一等公民”(first class),指的是函数与其他数据类型一样,处于平等地位,可以赋值给其他变量,也可以作为参数,传入另一个函数,或者作为别的函数的返回值。
可以在函数里定义其他函数,就好像在函数里定义整数一样。还可以定义匿名函数,并随意地插入到代码的任何地方。
函数作为头等结构的这种理念简化了操作符的抽象和新控制结构的创建。这种函数的泛化具有很强的表现力,是程序保持清晰易懂的保证,而且它还在可扩展性上扮演了重要的角色。
函数编程支持函数作为第一类对象,有时称为闭包(Closure)或者仿函数(functor)对象。实质上,闭包是起函数的作用并可以像对象一样操作的对象。与此类似,FP 语言支持高阶函数(Higher-order function)。高阶函数可以用另一个函数(间接地,用一个表达式) 作为其输入参数,在某些情况下,它甚至返回一个函数作为其输出参数。这两种结构结合在一起使得可以用优雅的方式进行模块化编程,这是使用 FP 的最大好处。
所谓”副作用”(side effect),指的是函数内部与外部互动(最典型的情况,就是修改全局变量的值),产生运算以外的其他结果。
函数式编程强调没有”副作用”,意味着函数要保持独立,方法与其所在环境交流的唯一方式应该是获得参数和返回结果,所有功能就是返回一个新的值,没有其他行为,尤其是不得修改外部变量的值。
因为 FP 语言不包含任何赋值语句,变量值一旦被指派就永远不会改变。而且,调用函数只会计算出结果 ── 不会出现其他效果。因此,FP 语言没有副作用。
惰性计算
FP 还引入了惰性计算(Lazy evaluation)的概念。在惰性计算中,表达式不是在绑定到变量时立即计算,而是在求值程序需要产生表达式的值时进行计算。延迟的计算使您可以编写可能潜在地生成无穷输出的函数。因为不会计算多于程序的其余部分所需要的值,所以不需要担心由无穷计算所导致的 out-of-memory 错误。一个惰性计算的例子是生成无穷 Fibonacci 列表的函数,但是对 第 n 个Fibonacci 数的计算相当于只是从可能的无穷列表中提取一项。
递归
FP 还有一个特点是用递归(recursive)做为控制流程的机制。例如,Lisp 处理的列表定义为在头元素后面有子列表,这种表示法使得它自己自然地对更小的子列表不断递归。
引用透明性
函数程序通常还加强引用透明性(Referential transparency),即如果提供同样的输入,那么函数总是返回同样的结果。就是说,表达式的值不依赖于可以改变值的全局状态。这使您可以从形式上推断程序行为,因为表达式的意义只取决于其子表达式而不是计算顺序或者其他表达式的副作用。这有助于验证正确性、简化算法,甚至有助于找出优化它的方法。
函数式编程可以使得代码简洁、开发快速、代码接近自然语言易于理解等,还有有关函数式编程自身特点带来的易用性。
函数式编程不依赖、也不会改变外界的状态,只要给定输入参数,返回的结果必定相同。因此,每一个函数都可以被看做独立单元,很有利于进行单元测试(unit testing)和除错(debugging),以及模块化组合。
不变性带来的另一个好处是:由于(多个线程之间)不共享状态,不会造成资源争用(Race condition),也就不需要用锁来保护可变状态,也就不会出现死锁,这样可以更好地并发起来,尤其是在对称多处理器(SMP)架构下能够更好地利用多个处理器(核)提供的并行处理能力。
函数式风格和指令式编程风格的代码差异,大致可以说,如果代码包含了任何var变量,那它可能就是指令式的风格。如果代码根本没有var,仅仅包含val,那它或许是函数式风格。向函数式风格转变的方式之一,就是尝试不用任何var编程。
//使用var的指令式风格
def printArgs(args: Array[String]): Unit = {
var i = 0;
while(i < args.length){
println(args(i))
i += 1
}
}
//函数式风格
def printArgs(args: Array[String]): Unit = {
for(arg <- args)
println(arg)
}
//或者
def printArgs(args: Array[String]): Unit = {
args.foreach(println)
}
上面这段代码仍然有修改的余地。重构后的printArgs方法并不是纯函数式的,因为它有副作用——打印到标准输出流。
识别函数是否有副作用的地方就在于其结果类型是否为Unit。如果某个函数不返回任何有用的值,也就是说如果返回类型是Unit,那么这个函数唯一能产生的作用就只能是通过某种副作用。
函数风格的方式应该是定义对需打印的arg进行格式化的方法,不过仅返回格式化之后的字符串:
def formatArgs(args: Array[String]) = args.mkString("\n")
现在才是真正的函数式风格:完全没有副作用或var的mkString方法,能在任何可枚举的集合类型(包括数组,列表,集合映射)上调用,返回由每个数组元素调用toString,并把传入字符串做分隔符组成的字符串。
提倡无副作用的方法的好处之一是可以有助于你的程序更容易测试。
在上面给出了例子。任何一个有副作用的printArgs方法,你需要重定义println,捕获传递给它的输出,在检查结果。对于formatArgs来说,你可以直接检查它的返回结果:
val res = format(Array("zero","one","two"))
assert(res == "zero\none\ntwo")
这里的assert方法检查传入的Boolean表达式,如果结果为假,抛出AssertionError;否则什么也不做。
转载请注明作者Jason Ding及其出处
GitCafe博客主页(http://jasonding1354.gitcafe.io/)
Github博客主页(http://jasonding1354.github.io/)
CSDN博客(http://blog.csdn.net/jasonding1354)
简书主页(http://www.jianshu.com/users/2bd9b48f6ea8/latest_articles)
百度搜索jasonding1354进入我的博客主页
原文地址:http://blog.csdn.net/jasonding1354/article/details/46009693