编程范式
函数式编程是一种编程范式,我们常见的编程范式有命令式编程(Imperative programming),函数式编程,逻辑式编程,常见的面向对象编程是也是一种命令式编程。
命令式编程是面向计算机硬件的抽象,有变量(对应着存储单元),赋值语句(获取,存储指令),表达式(内存引用和算术运算)和控制语句(跳转指令),一句话,命令式程序就是一个冯诺依曼机的指令序列。
而函数式编程是面向数学的抽象,将计算描述为一种表达式求值,一句话,函数式程序就是一个表达式。
函数式编程的本质
函数式编程中的函数这个术语不是指计算机中的函数(实际上是Subroutine),而是指数学中的函数,即自变量的映射。也就是说一个函数的值仅决定于函数参数的值,不依赖其他状态。比如sqrt(x)函数计算x的平方根,只要x不变,不论什么时候调用,调用几次,值都是不变的。
在函数式语言中,函数作为一等公民,可以在任何地方定义,在函数内或函数外,可以作为函数的参数和返回值,可以对函数进行组合。
纯函数式编程语言中的变量也不是命令式编程语言中的变量,即存储状态的单元,而是代数中的变量,即一个值的名称。变量的值是不可变的(immutable),也就是说不允许像命令式编程语言中那样多次给一个变量赋值。比如说在命令式编程语言我们写“x = x + 1”,这依赖可变状态的事实,拿给程序员看说是对的,但拿给数学家看,却被认为这个等式为假。
函数式编程的好处
由于命令式编程语言也可以通过类似函数指针的方式来实现高阶函数,函数式的最主要的好处主要是不可变性带来的。没有可变的状态,函数就是引用透明(Referential transparency)的和没有副作用(No Side Effect)。
一个好处是,函数即不依赖外部的状态也不修改外部的状态,函数调用的结果不依赖调用的时间和位置,这样写的代码容易进行推理,不容易出错。这使得单元测试和调试都更容易。
不变性带来的另一个好处是:由于(多个线程之间)不共享状态,不会造成资源争用(Race condition),也就不需要用锁来保护可变状态,也就不会出现死锁,这样可以更好地并发起来,尤其是在对称多处理器(SMP)架构下能够更好地利用多个处理器(核)提供的并行处理能力。
还有一个好处是,由于函数式语言是面向数学的抽象,更接近人的语言,而不是机器语言,代码会比较简洁,也更容易被理解。
函数式编程的特性
由于变量值是不可变的,对于值的操作并不是修改原来的值,而是修改新产生的值,原来的值保持不变。
同样由于变量不可变,纯函数编程语言无法实现循环,这是因为For循环使用可变的状态作为计数器,而While循环或DoWhile循环需要可变的状态作为跳出循环的条件。因此在函数式语言里就只能使用递归来解决迭代问题,这使得函数式编程严重依赖递归。
函数式语言当然还少不了以下特性:
- 高阶函数(Higher-order function)
- 偏应用函数(Partially Applied Functions)
- 柯里化(Currying)
- 闭包(Closure)
Java里的函数式编程
函数是“第一等公民”。
函数与其他数据类型一样,可以赋值给其他变量,也可以作为参数,也可以作为返回值。
不可变性
像闭包一样,传入的自由变量是不可变的,降低数据的不一致性。同时也只返回新的值,不修改变量状态,没有“副作用”。
Java里的lambda表达式
函数式编程的基础,用于创建函数式接口的实现对象。函数式接口就是只包含一个抽象方法的接口,如Runnable。@FunctionalInterface注解用于提供约束。
变量作用域
lambda表达式可以理解为匿名内部类的“语法糖”,但略有些不同。
- “捕获”传入参数,复制到生成的实例中
- 自由变量必须为有效final
- 与内部类不同,lambda表达式方法体中的变量与上层嵌套代码块有着相同的作用域,this也指向外层的对象。
总结
函数式编程是给软件开发者提供的另一套工具箱,为我们提供了另外一种抽象和思考的方式。
当对一组数据做加工时,先查询,然后聚合,聚合后排序,再join,再排序,再聚合,再转换(map)得到最终的结果。这个过程,用FP的函数就很自然。
函数式编程也有不太擅长的场合,比如处理可变状态和处理IO,要么引入可变变量,要么通过Monad来进行封装(如State Monad和IO Monad)