什么是函數(shù)式編程
函數(shù)式編程的概念誕生在二十世紀(jì)五十年代,近些年函數(shù)式編程獲得越來越多的關(guān)注,很多語言加入了函數(shù)式編程的支持。比如java 8 加入了lambda表達(dá)式。
函數(shù)式編程是一種編程范式,也就是如何編寫程序的方法論。通過組合函數(shù)的方式來編寫程序。函數(shù)式編程主張聲明式編程而非命令式編程。
函數(shù)式編程中的函數(shù)這個術(shù)語是指數(shù)學(xué)中的函數(shù),即自變量的映射y=f(x)。也就是說函數(shù)的值僅取決于函數(shù)的參數(shù),不依賴其它狀態(tài)。我們要使用數(shù)學(xué)函數(shù)的思想來理解函數(shù)式編程。
函數(shù)式編程的理論基礎(chǔ)是lambda演算,用函數(shù)組合的方式來描述計算過程,即一個問題如果能用一套函數(shù)組合的算法來表達(dá)那么這個問題是可計算的。
函數(shù)式編程的主要思想是把運算過程盡量寫成一系列的函數(shù)調(diào)用。比如:
(a+b)*c-d
函數(shù)式編程要求運算過程定義為不同的函數(shù):
subtract(multiply(add(a,b),c),d)
聲明式和命令式
函數(shù)式編程主張聲明式編程和編寫抽象的代碼。假設(shè)有一個數(shù)組,你想遍歷它并打印到控制臺。命令式寫法:
//命令寫法
var array = [1, 2, 3];
for (let index = 0; index < array.length; index++) {
console.log(array[index]);
}
為了解決問題,我們告訴程序該如何做:獲取數(shù)組長度,循環(huán)數(shù)組,用索引獲取每個元素。命令式編程主張告訴編譯器若何做。
在聲明式編程中,我們要告訴編譯器做什么,而不是如何做。如何做被抽象到普通函數(shù)中。聲明式寫法:
var array = [1, 2, 3];
// 聲明式寫法
array.forEach((element) => console.log(element));
我們使用了如何做的抽象函數(shù)forEach,如此可以讓開發(fā)者只關(guān)心做什么的部分。把操作抽象為函數(shù)是函數(shù)式編程的核心思想。我們把循環(huán)的操作抽象為函數(shù),以便在需要時可以重用:
const forEach = (array, fn) => {
for (let i = 0; i < array.length; i++) fn(array[i]);
};
函數(shù)式編程5個特點
函數(shù)是一等公民
函數(shù)和普通數(shù)據(jù)類型一樣,可以作為參數(shù)傳遞,可以賦值,可以作為函數(shù)的返回值。
函數(shù)沒有副作用
對同樣的輸入,總是返回相同的輸出,不能修改外部變量的函數(shù)稱為純函數(shù),否則稱該函數(shù)是有副作用的。一個沒有副作用的函數(shù):
const double = (value) => value * 2;
一個有副作用的函數(shù):
let discount = 0.8;
let price = (value) => discount * value;
// price依賴外部變量discount
price函數(shù)依賴外部變量discount,那么:
price(10) === 8
如果discount變化了,那么:
discount = 0.6;
price(10) === 6;
對同樣的輸入輸出不同那么price函數(shù)是有副作用的。
純函數(shù)不應(yīng)該修改外部變量,例如:
// 修改外部變量
var global = "value";
var badFunction = (value) => {
global = "value2";
return value * 2;
};
badFunction函數(shù)修改了global變量,調(diào)用badFunction函數(shù)則影響了其它函數(shù)的行為。
引用透明性
對于同樣的輸入都將返回相同的值,函數(shù)的這一屬性被稱為引用透明性。例如:
var double = (value) => value * 2;
// double(2) 可以用 4 替換
利用引用透明性我們可以緩存函數(shù)的值,比如我們有一個計算階乘的函數(shù)factorial,我們知道5的階乘是120,當(dāng)?shù)诙斡嬎?的階乘時不用重新計算了,直接使用緩存的值即可。
數(shù)據(jù)是不可變的
在純的函數(shù)式編程語言中,數(shù)據(jù)是不可變的,沒有變量的概念。所有的數(shù)據(jù)一旦產(chǎn)生就不能改變它的值,如果要改變只能生成新的數(shù)據(jù)。
只使用表達(dá)式不使用語句
函數(shù)式編程要求只使用表達(dá)式不使用語句,也就是每一步都是單純的運算,并且有返回值。
函數(shù)式編程舉例
打印數(shù)組中偶數(shù)的值
命令式編程:
// 查找列表中的偶數(shù)
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);
}
}
函數(shù)式編程要把操作過程抽象到函數(shù)中,把循環(huán)過程抽象到forEach 函數(shù)中,判斷是否為偶數(shù)if (element % 2 === 0){}抽象到unless函數(shù)中,調(diào)用:
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以內(nèi)的奇數(shù)
// 命令式寫法
for (let index = 0; index < 100; index++) {
if (index % 2) {
console.log(index);
}
}
函數(shù)式寫法:
function times(num, fn) {
for (let index = 0; index < num; index++) {
fn(index);
}
}
function unless(predicate, fn) {
if (predicate) {
fn();
}
}
// 100 以內(nèi)的奇數(shù)
times(100, (index) => {
unless(index % 2, () => {
console.log(index);
});
});
數(shù)組函數(shù)式編程綜合應(yīng)用
統(tǒng)計 books 評價 good 和 excellent 的數(shù)量,數(shù)據(jù)結(jié)構(gòu)如下:
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]
);
}
函數(shù)式寫法:
// 省略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]
);
}
函數(shù)式庫
- ramada: https://github.com/ramda/ramda
- lodash/fp: https://github.com/lodash/lodash/wiki/FP-Guide
參考
- https://en.wikipedia.org/wiki/Functional_programming
- es6函數(shù)式編程入門經(jīng)典
- https://zhuanlan.zhihu.com/p/28712866