什么是函数式编程

简单说,「函数式编程」是一种「编程范式」,也就是如何编写程序的方法论。

它属于「结构化编程」结构化编程的一种,主要思想是把运算过程尽量写成一系列嵌套的函数调用。举例来说,现在有这样一个数学表达式:

(1 + 2) * 3 - 4

传统的过程式编程,可能这样写:

var a = 1 + 2;

var b = a * 3;

var c = b - 4;

函数式编程要求使用函数,我们可以把运算过程定义为不同的函数,然后写成下面这样:

var result = subtract(multiply(add(1,2), 3), 4);

这就是函数式编程。

编程范式

bianchengfanshi

程序编程范式。一个是左脑,一个右脑。我们程序员基本上是在用左脑,左脑是理性分析,喜欢数据证据,线性思维,陷入细节,具体化的,不抽象。但是实际上玩儿出这些东西的都在右脑,函数式,还有像逻辑式的抽象能力都在右脑。所以我们非线性的想象力都在这边,而标准化教育把我们这边已经全部干掉了,我们只剩左边。

函数式编程或是声明式编程,需要的是用我们的右脑,而指令式的则需要用我们的左脑。很多人理解和使用声明式的编程范式比较有困难,因为这要用你的右脑,但是我们习惯于用我们的左脑,左脑用多了以后右脑就有点跟不上了。

younao

函数式编程的特性

函数是第一等公民

所谓「第一等公民」,指的是函数与其他数据类型一样,处于平等地位,它不仅拥有一切传统函数的使用方式(声明和调用),可以赋值给其他变量(赋值),也可以作为参数,传入另一个函数(传参),或者作为别的函数的返回值(返回)。函数可以作为参数进行传递,意味我们可以把行为「参数化」,处理逻辑可以从外部传入,这样程序就可以设计得更灵活

1
2
3
4
5
6
7
8
9
    function test(opt) {
        var result = opt(1, 2);
        console.log(result);
    }

    function add(a, b) {
        return a + b;
    }
    test(add);
1
2
3
  var print = function(i){ console.log(i);};

  [1,2,3].forEach(print);

没有副作用

指的是函数内部与外部互动(最典型的情况,就是修改全局变量的值),产生运算以外的其他结果。函数式编程强调没有「副作用」,意味着函数要保持独立,所有功能就是返回一个新的值,没有其他行为,尤其是不得修改外部变量的值。

函数式编程只是返回新的值,不修改系统变量。因此,不修改变量,也是它的一个重要特点。

引用透明

指的是函数的运行不依赖于外部变量或「状态」,只依赖于输入的参数,任何时候只要参数相同,引用函数所得到的返回值总是相同的。这里强调了一点:输入不变则输出也不变,就像数学函数里面的f(x),只要输入的x一样那得到的结果也肯定定是一样的。

函数式编程的意义

代码简洁,开发快速。

函数式编程大量使用函数,减少了代码的重复,因此程序比较短,开发速度较快。Paul Graham在《黑客与画家》一书中写道:同样功能的程序,极端情况下,Lisp代码的长度可能是C代码的二十分之一。如果程序员每天所写的代码行数基本相同,这就意味着,“C语言需要一年时间完成开发某个功能,Lisp语言只需要不到三星期。反过来说,如果某个新功能,Lisp语言完成开发需要三个月,C语言需要写五年。“当然,这样的对比故意夸大了差异,但是"在一个高度竞争的市场中,即使开发速度只相差两三倍,也足以使得你永远处在落后的位置。”

接近自然语言,易于理解

函数式编程的自由度很高,可以写出很接近自然语言的代码。以 Java 为例把学生以性别分组:

没用 Lambda 表达式

1
2
3
4
5
6
7
Map<String,List<Student>> studentsMap = new HashMap<>();

for(Student student : students){
  List<Student> studentList = studentsMap.getOrDefault(student.getSex(),new ArrayList<>());
  studentList.add(student);
  studentsMap.put(student.getSex(),studentList);
}	

用了 Lambda 表达式:

1
Map<String,List<Student>> studentsMap = students.stream().collect(Collectors.groupingBy(Student::getSex));

这基本就是自然语言的表达了,大家应该一眼就能明白它的意思吧。

更方便的代码管理

函数式编程不依赖、也不会改变外界的状态,只要给定输入参数,返回的结果必定相同。因此,每一个函数都可以被看做独立单元,很有利于进行单元测试和 Debug,以及模块化组合。

易于并发编程

函数式编程不需要考虑死锁,因为它不修改变量,所以根本不存在"锁"线程的问题。不必担心一个线程的数据,被另一个线程修改,所以可以很放心地把工作分摊到多个线程,部署并发编程

请看下面的代码:

1
2
3
var s1 = Op1();
var s2 = Op2();
var s3 = concat(s1, s2);

由于 s1 和 s2 互不干扰,不会修改变量,谁先执行是无所谓的,所以可以放心地增加线程,把它们分配在两个线程上完成。其他类型的语言就做不到这一点,因为 s1 可能会修改系统状态,而 s2 可能会用到这些状态,所以必须保证 s2 在 s1 之后运行,自然也就不能部署到其他线程上了。多核 CPU 是将来的潮流,所以函数式编程的这个特性非常重要。

Java8 的 Lambda 表达式

Java8 中 Lambda 表达式由三个部分组成:第一部分为一个括号内用逗号分隔的形式参数,参数是函数式接口里面方法的参数;第二部分为一个箭头符号:->;第三部分为方法体,可以是表达式和代码块。语法如下 方法体为表达式,该表达式的值作为返回值返回。

1
2
(parameters) -> expression
(int a,int b) -> return a + b; //求和

方法体为代码块,必须用 {} 来包裹起来,且需要一个 return 返回值,但若函数式接口里面方法返回值是 void,则无需返回值。

1
2
3
(parameters) -> { statements; }
(int a) -> {System.out.println("a = " + a);} //打印,无返回值
(int a) -> {return a * a;} //求平方

能够接收 Lambda 表达式的参数类型,是一个只包含一个方法的接口。只包含一个方法的接口称之为函数接口,通常用@FunctionalInterface注解修饰。

1
2
3
4
@FunctionalInterface
public interface IOpt {
	int opt(int a, int b);
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
	@Test
	public void test() {
		// 匿名内部类的方式
		int result1 = func(new IOpt() {
			@Override
			public int opt(int a, int b) {
				return a + b;
			}
		});
		// Lambda 的方式
		int result = func((a, b) -> a - b);
		System.out.println(result);
	}

	private int func(IOpt functionInterface) {
		int a = 2;
		int b = 6;
		return functionInterface.opt(a, b);
	}

由此可以看出 Lambda 表达式的主要用途就是替换匿名类,而实现 Runnable 接口是匿名类的最好示例。看一下 Java8 之前的 Runnable 实现方法,需要 4 行代码,而使用 Lambda 表达式只需要一行代码。我们在这里做了什么呢?那就是用() -> {}代码块替代了整个匿名类。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
	@Test
	public void testRunnable() {
		// Java 8之前:
		new Thread(new Runnable() {
			@Override
			public void run() {
				System.out.println("Before Java8, too much code for too little to do");
			}
		}).start();

		// Java 8方式:
		new Thread(() -> System.out.println("In Java8, Lambda expression rocks !!")).start();

	}

最佳实践

保持Lambda表达式简短和一目了然

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
values.stream()
  .mapToInt(e -> {    
    int sum = 0;
    for(int i = 1; i <= e; i++) {
      if(e % i == 0) {
        sum += i;
      }
    }  
    return sum;
  }).sum());  //代码复杂难懂 
  
values.stream()
  .mapToInt(e -> sumOfFactors(e)).sum() //代码简洁一目了然

长长的Lambda表达式通常是危险的,因为代码越长越难以读懂,意图看起来也不明,并且代码也难以复用,测试难度也大。

使用@FunctionalInterface 注解

如果你确定了某个 interface 是用于 Lambda 表达式,请一定要加上@FunctionalInterface,表明你的意图。不然将来说不定某个不知情的家伙比如你旁边的好基友,在这个 interface 上面加了另外一个抽像方法时,你的代码就悲剧了。

优先使用java.util.function包下面的函数式接口

java.util.function这个包下面提供了大量的功能性接口,可以满足大多数开发人员为 Lambda 表达式和方法引用提供目标类型的需求。每个接口都是通用的和抽象的,使它们易于适应几乎任何 Lambda 表达式。开发人员应该在创建新的功能接口之前研究这个包,避免重复定义接口。另外一点就是,里面的接口不会被别人修改。

不要在Lambda表达中执行有副作用的操作

副作用是严重违背函数式编程的设计原则,在工作中我经常看到有人在 forEach 操作里面操作外面的某个 List 或者设置某个 Map 这其实是不对的。

不要把 Lambda 表达式和匿名内部类同等对待

虽然我们可以用匿名内部类来实现 Lambda 表达式,也可以用 Lambda 表达式来替换内部类,但并不代表这两者是等价的。这两者在某一个重要概念是不同的:this指代的上下文是不一样的。当您使用内部类时,它将创建一个新的范围。通过实例化具有相同名称的新局部变量,可以从封闭范围覆盖局部变量。您还可以在内部类中使用这个关键字作为它实例的引用。但是,Lambda表达式可以使用封闭范围。您不能在 Lambda的主体内覆盖范围内的变量

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
private String value = "Enclosing scope value";
 
public String scopeExperiment() {
    Foo fooIC = new Foo() {
        String value = "Inner class value";
        @Override
        public String method(String string) {
            return this.value;
        }
    };
    String resultIC = fooIC.method("");
  
    Foo fooLambda = parameter -> {
        String value = "Lambda value";
        return this.value;
    };
    String resultLambda = fooLambda.method("");
  
    return "Results: resultIC = " + resultIC +
      ", resultLambda = " + resultLambda;
}

运行上面这段代码我们将到resultIC = "Inner class value",resultLambda = "Enclosing scope value"。也就是说在匿名内部类中this指的是自身的引用,在Lambda 表达式中this指的是外部。

多使用方法引用

在 Lambda 表达式中a -> a.toLowerCase()String::toLowerCase都能起到相同的作用,但两者相比,后者通常可读性更高并且代码会简短。

尽量避免在 Lambda 的方法体中使用 {} 代码块

优先使用

1
2
3
4
5
6
Foo foo = parameter -> buildString(parameter);
private String buildString(String parameter) {
    String result = "Something " + parameter;
    //many lines of code
    return result;
}

而不是

1
2
3
4
5
Foo foo = parameter -> { 
    String result = "Something " + parameter;
    //many lines of code
    return result;
};