函数式编程范式初探-第一天

前言

最近在翻阅一些技术论坛的时候, 常常能看到有人提及函数式编程(FunctionalProgramming), 说他可以简化我们的代码, 提高我们的代码生产力以及降低出错的几率.

作为一个希望提高开发效率的半吊子程序员来说, 我觉得这应该是一个非常有利的’武器’, 值得去学习, 吸收其中的一些精华思想, 如果能看到可以开花结果的那一天, 那就再好不过了.

本文为自我学习总结文章, 正确性不保证, 我更推荐您阅读<<函数式编程思维>>[美]Neal Ford著这种不错的书籍

本文主要使用js和java来说明, 因为这两个是我目前工作的常用语言, 对我自己的好处最大. 如果你想享受更好的函数式编程体验, 可以试试Clojure或者Scala

是什么为什么

我这人对一个技术的起源不是非常感兴趣, 所以我不想多费时间减少函数式编程的历史, 我想直接从函数式编程最常用的技术点讲起, 来让我快速的了解它

以函数为抽象单元

我们在面向对象编程中, 一般都会认为一个类Object是一个抽象单元, 比如在Java中(一门完全以面向对象为基础的编程语言), 我们都要从编写类开始.

而函数式编程的第一个特点就是, 它把函数视为抽象单元, 我们可以传递函数, 以函数作为参数, 大家看看下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let input = ['1','2','3'];

function listForEach(list,operate){
for (const key in list) {
operate(key);
}
}

function consoleKey(key){
//do something.
console.log(key);
}

listForEach(input, consoleKey);

这里我用Js实现了一段代码, 这段代码会依次打印数组中的值. 这里的最大特点就是consoleKey是个函数, 并且我们将其作为listForEach的参数传入了. 所以对于listForEach而言, for循环内部具体执行什么逻辑, 是可以由具体传入了什么函数决定的. 这里函数就是一个抽象的单元, 可以根据我们的需要进行任意的替换.

我写完这个例子后产生了一些疑问, 因为在我看来, 这就是简单的一个抽象化调用而已, 在我还没学习函数式编程语言之前, 我也经常会写这样的代码, 并且我当时觉得这样的写法也并没有简化多少. 这个问题, 我也自己在不断的考虑, 最后我觉得这是一种比较主观的问题, 不同人对于代码复杂度有不同的判断, 有些人觉得几百行的crud代码一点也不复杂, 因为分解一下, 代码内容其实就只有crud罢了; 而可能有些人则会觉得, 这些代码就像屎山一样, 难以下手.

还记得我最早接触编程的时候, 学的是c, 所以当时写的都是命令式的代码. 那么如果按照命令式的写法, 我上面的例子会怎么写呢? 如下是我的按照命令式的写法改写的例子:

1
2
3
4
5
let input = ['1','2','3'];

for (const key in list) {
console.log(key);
}

嗯? 怎么感觉命令式的写法更加的简单? 在这里确实是这样的没错. 但是没够多久, 项目经理要求先判断下元素是否为空, 这时候对于函数式编程来说, 写法是新增一个函数抽象, 然后替换我们传入的函数, 就像下面这样:

1
2
3
4
5
6
7
8
9
//...原函数省略
function consoleKeyNotNull(key){
//检查是否为null
if(key){
console.log(key);
}
}

listForEach(input, consoleKeyNotNull);

我们需要改的是新增一个consoleKeyNotNull函数, 以及修改传入的函数即可. 这样的写法符合软件开发的开闭原则, 我们没有去修改原来的函数代码.

相同的修改放到命令式语法上就有点棘手了, 因为你需要修改源代码, 并且如果项目经理突然变卦让你把控制去掉, 你又得自己改回去, 这种需求如果不断的堆积, 对代码的维护会造成不小的压力.

此外,由于是函数, 所以一般来说都会给函数一个命名, 所以我们在查阅这些代码的时候, 如果函数命名特别贴切的话, 会一下子提高阅读源码的速度, 我觉得这也是函数式的一个小特点

此外,我们可能还需要了解下一种说法, 那就是这种使用函数的函数, 我们一般就叫做高阶函数, 一个更加高大上的说法:)

纯函数

函数式编程的另一个特点是, 要求函数针对同一个输入有一个相同的输出, 这个概念就和我们小时候学的方程式很类似, 比如方程x + y, 对于给定的输入x和y, 我们可以得出固定的答案. 函数式编程也极大的推崇这个特点. 因为这个特点可以提高我们这个函数的复用性

试想一下, 如果一个函数是纯函数, 那么任何需要用到它的地方, 都可以放心大胆的使用它, 并且有时候我们可以不对其进行单元测试, 这样对于测试来说, 相当于减少了不稳定要素, 而不稳定要素的减少, 肯定会减少代码bug的出现. 因为很多时候bug都是因为不稳定要素的过多, 导致程序员无法一口气把所有的情况都考虑到而产生的.

对于纯函数, 我能想到的例子还是很多的, 比如sin(x),abs(x),sqrt(x), 这些函数对于我们的指定的输入, 总是会返回同一个结果.

不过个人认为完全纯函数是不可能实现的, 因为在实际的开发过程中, 我们面对的是更为复杂的业务场景, 不稳定要素非常多, 比如超时问题,网络问题等等, 所以我觉得函数式编程作为一个编程范式, 它的作用仍然没有面向对象那么重要. 但是在针对一些算法, 一些小的场景下, 使用函数式编程范式, 确实是可以提高一定的生产力

lambda表达式

在函数式编程中, lambda表达式是我们的常见的家伙. 由于函数式编程就是一直在写函数, 所以对于一些不需要函数名的场景, lambda表达式可以发挥出简化代码, 提高可读性的作用

lambda表达式的语法也许不同语言不一致, 但是几乎都如出一辙, 下面我用java语言来演示一些:

1
2
3
4
5
6
7
8
9
10
11
//传统写法
new Thread(new Runnable() {
public void run() {
System.out.println("Anonymous");
}
}).start();

//使用lambda表达式
new Thread(
() -> System.out.println("lambda")
).start();

可以看到, lambda表达式的作用其实就是省略到了很多不必要的语法, 只剩下我们真正关心的东西, 比如()代表这个方法没有形参, ->是lambda表达式的核心语法符号, 有表示产出的含义. 最后的System.out.println("lambda") 就是方法体, 由于单行, 所以这里省略了花括号

怎么样? 是不是非常简洁? 不过这只是lambda表达式的初探, 我后续会对其进行详细的说明, 本文主要阐述FP的核心概念, 并不会对具体语法做过多说明

lambda表达式为函数的编写提供了一种更易读的方式, 支持函数式编程的语言, 都有提供lambda表达式功能, 它是函数式编程之所以简洁的其中一个原因!

Stream && 将控制权交给语言/运行时

最能体会到函数式编程乐趣的方式, 其实就是使用Stream.

Stream内置了大量的函数操作, 使得我们可以借助这些函数来完成各种操作, 最有名的三大函数就是map/filter/reduce

我会在后续进行Stream的总结(不过是只针对java的), 我们可以从Stream的这些函数操作中, 深刻的体会到函数式编程的魅力! 我给一个简单的Stream的例子吧:

比如我们需要随机展示 5 至 20 之间不重复的整数并进行排序, 那么传统的方式可能是下面这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
import java.util.*;
public class ImperativeRandoms {
public static void main(String[] args) {
Random rand = new Random(47);
SortedSet<Integer> rints = new TreeSet<>();
while(rints.size() < 7) {
int r = rand.nextint(20);
if(r < 5) continue;
rints.add(r);
}
System.out.println(rints);
}
}

而函数式编程&流后,代码可以变得异常的简单:

1
2
3
4
5
6
7
8
9
10
11
import java.util.*;
public class Randoms {
public static void main(String[] args) {
new Random(47)
.ints(5, 20)
.distinct()
.limit(7)
.sorted()
.forEach(n -> System.out.println(n););
}
}

怎么样? 这里我们借助了StreamAPI中的各种函数为我们达到了我们要做的目的, 整体代码看上去犹如读一句话一样流畅, 先ints获取数据源, 然后distinct一下排除重复, 然后我们坐下limit限制, 然后再sorted以下让其有序, 最后forEach一下, forEach的内容就是对每个元素进行打印!

总而言之, 函数式编程的实现会看上去非常的有可读性, 而传统的写法则需要对java语法有一定基础, 才有能力去编写出能用的代码. 不得不说其实这种声明式的写法很大程度上又是对开发人员技术要求的降低, 但是不代表我们不需要去了解其内部的原理. 在这里所有的操作都被移交给了运行时, 不需要我们亲自编写for循环,编写控制代码.

总结

本文对函数式编程进行一个基础性的表述, 下一章我会使用java语言, 详细的说明java的函数式编程支持语法,

Live2d