什么是函數(shù)式編程
函數(shù)式編程的概念誕生在二十世紀(jì)五十年代,近些年函數(shù)式編程獲得越來越多的關(guān)注,很多語言加入了函數(shù)式編程的支持。比如java 8 加入了lambda表達(dá)式。
函數(shù)式編程是一種編程范式凛膏,也就是如何編寫程序的方法論。通過組合函數(shù)的方式來編寫程序它浅。函數(shù)式編程主張聲明式編程而非命令式編程译柏。
函數(shù)式編程中的函數(shù)這個(gè)術(shù)語是指數(shù)學(xué)中的函數(shù),即自變量的映射y=f(x)姐霍。也就是說函數(shù)的值僅取決于函數(shù)的參數(shù)鄙麦,不依賴其它狀態(tài)典唇。我們要使用數(shù)學(xué)函數(shù)的思想來理解函數(shù)式編程。
函數(shù)式編程的理論基礎(chǔ)是lambda演算胯府,用函數(shù)組合的方式來描述計(jì)算過程介衔,即一個(gè)問題如果能用一套函數(shù)組合的算法來表達(dá)那么這個(gè)問題是可計(jì)算的。
函數(shù)式編程的主要思想是把運(yùn)算過程盡量寫成一系列的函數(shù)調(diào)用骂因。比如:
(a+b)*c-d
函數(shù)式編程要求運(yùn)算過程定義為不同的函數(shù):
subtract(multiply(add(a,b),c),d)
聲明式和命令式
函數(shù)式編程主張聲明式編程和編寫抽象的代碼炎咖。假設(shè)有一個(gè)數(shù)組,你想遍歷它并打印到控制臺寒波。命令式寫法:
//命令寫法
var array = [1, 2, 3];
for (let index = 0; index < array.length; index++) {
console.log(array[index]);
}
為了解決問題乘盼,我們告訴程序該如何做:獲取數(shù)組長度,循環(huán)數(shù)組俄烁,用索引獲取每個(gè)元素绸栅。命令式編程主張告訴編譯器若何做。
在聲明式編程中页屠,我們要告訴編譯器做什么粹胯,而不是如何做。如何做被抽象到普通函數(shù)中辰企。聲明式寫法:
var array = [1, 2, 3];
// 聲明式寫法
array.forEach((element) => console.log(element));
我們使用了如何做的抽象函數(shù)forEach风纠,如此可以讓開發(fā)者只關(guān)心做什么的部分。把操作抽象為函數(shù)是函數(shù)式編程的核心思想牢贸。我們把循環(huán)的操作抽象為函數(shù)竹观,以便在需要時(shí)可以重用:
const forEach = (array, fn) => {
for (let i = 0; i < array.length; i++) fn(array[i]);
};
函數(shù)式編程5個(gè)特點(diǎn)
函數(shù)是一等公民
函數(shù)和普通數(shù)據(jù)類型一樣,可以作為參數(shù)傳遞潜索,可以賦值栈幸,可以作為函數(shù)的返回值。
函數(shù)沒有副作用
對同樣的輸入帮辟,總是返回相同的輸出,不能修改外部變量的函數(shù)稱為純函數(shù)玩焰,否則稱該函數(shù)是有副作用的由驹。一個(gè)沒有副作用的函數(shù):
const double = (value) => value * 2;
一個(gè)有副作用的函數(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ù)的值澜搅,比如我們有一個(gè)計(jì)算階乘的函數(shù)factorial伍俘,我們知道5的階乘是120,當(dāng)?shù)诙斡?jì)算5的階乘時(shí)不用重新計(jì)算了勉躺,直接使用緩存的值即可癌瘾。
數(shù)據(jù)是不可變的
在純的函數(shù)式編程語言中,數(shù)據(jù)是不可變的饵溅,沒有變量的概念妨退。所有的數(shù)據(jù)一旦產(chǎn)生就不能改變它的值,如果要改變只能生成新的數(shù)據(jù)蜕企。
只使用表達(dá)式不使用語句
函數(shù)式編程要求只使用表達(dá)式不使用語句咬荷,也就是每一步都是單純的運(yùn)算,并且有返回值轻掩。
函數(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)計(jì) books 評價(jià) 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