什么是函数式编程
函数式编程的概念诞生在二十世纪五十年代,近些年函数式编程获得越来越多的关注,很多语言加入了函数式编程的支持。比如java 8 加入了lambda表达式。
函数式编程是一种编程范式,也就是如何编写程序的方法论。通过组合函数的方式来编写程序。函数式编程主张声明式编程而非命令式编程。
函数式编程中的函数这个术语是指数学中的函数,即自变量的映射y=f(x)。也就是说函数的值仅取决于函数的参数,不依赖其它状态。我们要使用数学函数的思想来理解函数式编程。
函数式编程的理论基础是lambda演算,用函数组合的方式来描述计算过程,即一个问题如果能用一套函数组合的算法来表达那么这个问题是可计算的。
函数式编程的主要思想是把运算过程尽量写成一系列的函数调用。比如:
(a+b)*c-d
函数式编程要求运算过程定义为不同的函数:
subtract(multiply(add(a,b),c),d)
声明式和命令式
函数式编程主张声明式编程和编写抽象的代码。假设有一个数组,你想遍历它并打印到控制台。命令式写法:
//命令写法
var array = [1, 2, 3];
for (let index = 0; index < array.length; index++) {
console.log(array[index]);
}
为了解决问题,我们告诉程序该如何做:获取数组长度,循环数组,用索引获取每个元素。命令式编程主张告诉编译器若何做。
在声明式编程中,我们要告诉编译器做什么,而不是如何做。如何做被抽象到普通函数中。声明式写法:
var array = [1, 2, 3];
// 声明式写法
array.forEach((element) => console.log(element));
我们使用了如何做的抽象函数forEach,如此可以让开发者只关心做什么的部分。把操作抽象为函数是函数式编程的核心思想。我们把循环的操作抽象为函数,以便在需要时可以重用:
const forEach = (array, fn) => {
for (let i = 0; i < array.length; i++) fn(array[i]);
};
函数式编程5个特点
函数是一等公民
函数和普通数据类型一样,可以作为参数传递,可以赋值,可以作为函数的返回值。
函数没有副作用
对同样的输入,总是返回相同的输出,不能修改外部变量的函数称为纯函数,否则称该函数是有副作用的。一个没有副作用的函数:
const double = (value) => value * 2;
一个有副作用的函数:
let discount = 0.8;
let price = (value) => discount * value;
// price依赖外部变量discount
price函数依赖外部变量discount,那么:
price(10) === 8
如果discount变化了,那么:
discount = 0.6;
price(10) === 6;
对同样的输入输出不同那么price函数是有副作用的。
纯函数不应该修改外部变量,例如:
// 修改外部变量
var global = "value";
var badFunction = (value) => {
global = "value2";
return value * 2;
};
badFunction函数修改了global变量,调用badFunction函数则影响了其它函数的行为。
引用透明性
对于同样的输入都将返回相同的值,函数的这一属性被称为引用透明性。例如:
var double = (value) => value * 2;
// double(2) 可以用 4 替换
利用引用透明性我们可以缓存函数的值,比如我们有一个计算阶乘的函数factorial,我们知道5的阶乘是120,当第二次计算5的阶乘时不用重新计算了,直接使用缓存的值即可。
数据是不可变的
在纯的函数式编程语言中,数据是不可变的,没有变量的概念。所有的数据一旦产生就不能改变它的值,如果要改变只能生成新的数据。
只使用表达式不使用语句
函数式编程要求只使用表达式不使用语句,也就是每一步都是单纯的运算,并且有返回值。
函数式编程举例
打印数组中偶数的值
命令式编程:
// 查找列表中的偶数
var array = [1, 2, 3, 4, 8];
for (let index = 0; index < array.length; index++) {
const element = array[index];
if (element % 2 === 0) {
console.log(element);
}
}
函数式编程要把操作过程抽象到函数中,把循环过程抽象到forEach 函数中,判断是否为偶数if (element % 2 === 0){}抽象到unless函数中,调用:
const forEach = (array, fn) => {
for (let index = 0; index < array.length; index++) {
const element = array[index];
fn(element);
}
};
const unless = (num, fn) => {
if (num % 2 === 0) {
fn();
}
};
forEach(array, (element) => {
unless(element, () => console.log(element));
});
100以内的奇数
// 命令式写法
for (let index = 0; index < 100; index++) {
if (index % 2) {
console.log(index);
}
}
函数式写法:
function times(num, fn) {
for (let index = 0; index < num; index++) {
fn(index);
}
}
function unless(predicate, fn) {
if (predicate) {
fn();
}
}
// 100 以内的奇数
times(100, (index) => {
unless(index % 2, () => {
console.log(index);
});
});
数组函数式编程综合应用
统计 books 评价 good 和 excellent 的数量,数据结构如下:
let apressBooks2 = [
{
name: "beginners",
bookDetails: [
{
id: 111,
title: "C# 6.0",
author: "ANDREW TROELSEN",
rating: [4.7],
reviews: [{ good: 4, excellent: 12 }],
},
{
id: 222,
title: "Efficient Learning Machines",
author: "Rahul Khanna",
rating: [4.5],
reviews: [],
},
],
},
{
name: "pro",
bookDetails: [
{
id: 333,
title: "Pro AngularJS",
author: "Adam Freeman",
rating: [4.0],
reviews: [],
},
{
id: 444,
title: "Pro ASP.NET",
author: "Adam Freeman",
rating: [4.2],
reviews: [{ good: 14, excellent: 12 }],
},
],
},
];
过程式写法:
function p() {
const allBooks = apressBooks2.map((e) => e.bookDetails);
const concatBooks = [];
allBooks.forEach((e) => concatBooks.push(...e));
const allPreviews = concatBooks.map((e) => e.reviews);
const concatPreviews = allPreviews.filter((e) => e.length).map((e) => e[0]);
return concatPreviews.reduce(
(acc, e) => [acc[0] + e.good, acc[1] + e.excellent],
[0, 0]
);
}
函数式写法:
// 省略reduce, concatAll, map等Api
function fn() {
return reduce(
concatAll(
map(concatAll(map(apressBooks2, (e) => e.bookDetails)), (e) => e.reviews)
),
(acc, e) => [acc[0] + e.good, acc[1] + e.excellent],
[0, 0]
);
}
函数式库
- ramada: https://github.com/ramda/ramda
- lodash/fp: https://github.com/lodash/lodash/wiki/FP-Guide