[TOC]
# 范疇論
## 范疇
范疇就是使用箭頭連接的物體。也就是說,彼此之間存在某種關系的概念、事物、對象等等,都構成"范疇"。隨便什么東西,只要能找出它們之間的關系,就能定義一個"范疇"。
<br>

<br>
上圖中,各個點范疇的成員,與它們之間的箭頭,就構成一個范疇。
<br>
箭頭表示范疇成員之間的關系,正式的名稱叫做"態射"(morphism)。范疇論認為,同一個范疇的所有成員,就是不同狀態的"變形"(transformation)。通過"態射",一個成員可以變形成另一個成員。
<br>
<br>
## 范疇與容器
我們可以把"范疇"想象成是一個容器,里面包含兩樣東西。
* 值(value)
* 值的變形關系,也就是函數。
<br>
下面我們使用代碼,定義一個簡單的范疇。
```
class Category {
constructor(val) {
this.val = val;
}
addOne(x) {
return x + 1;
}
}
```
上面代碼中,Category是一個類,也是一個容器,里面包含一個值(this.val)和一種變形關系(addOne)。你可能已經看出來了,這里的范疇,就是所有彼此之間相差1的數字。
<br>
## 范疇論與函數式編程的關系
范疇論使用函數,表達范疇之間的關系。
<br>
伴隨著范疇論的發展,就發展出一整套函數的運算方法。這套方法起初只用于數學運算,后來有人將它在計算機上實現了,就變成了今天的"函數式編程"。
<br>
本質上,函數式編程只是范疇論的運算方法,跟數理邏輯、微積分、行列式是同一類東西,都是數學方法,只是碰巧它能用來寫程序。
<br>
<br>
# 函數式編程的特點
* 函數是”第一等公民”。所謂”第一等公民”(first class),指的是函數與其他數據類型一樣,處于平等地位,可以賦值給其他變量,也可以作為參數,傳入另一個函數,或者作為別的函數的返回值。
* 只用”表達式",不用"語句"。
* 沒有”副作用"。如果函數與外部可變狀態進行交互,則它是有副作用的。如:
* 修改一個變量
* 直接修改數據結構
* 設置一個對象的成員
* 拋出一個異常或以一個錯誤終止
* 打印到終端或讀取用戶的輸入
* 讀取或寫入一個文件
* 在屏幕上繪畫
* 不修改狀態。在函數式編程中變量僅僅代表某個表達式。這里所說的’變量’是不能被修改的。所有的變量只能被賦一次初值。
* 引用透明(函數運行只靠參數)
<br>
<br>
# 函數式編程的優點與缺點
優點
* 效降低系統的復雜度
* 可緩存性
```
import _ from 'lodash';
var sin = _.memorize(x =>Math.sin(x));
//第一次計算的時候會稍慢一點
var a = sin(1);
//第二次有了緩存,速度極快
var b = sin(1);
```
<br>
缺點
* 擴展性比較差
```
// 不純的
var min = 18;
var checkage = age => age > min;
// 純函數
var checkage = age => age > 18;
```
在不純的版本中,checkage 不僅取決于 age還有外部依賴的變量 min。純的 checkage 把關鍵數字 18 硬編碼在函數內部,,柯里化優雅的函數式解決。
<br>
<br>
# 核心概念
## 純函數(Purity)
對于相同的輸入,永遠會得到相同的輸出,而且沒有任何可觀察的副作用,也不依賴外部環境的狀態。
```
const greet = (name) => `hello, ${name}`
greet('world')
```
<br>
以下代碼函數依賴外部狀態,不是純函數:
```
window.name = 'Brianne'
const greet = () => `Hi, ${window.name}`
greet() // "Hi, Brianne"
```
<br>
以下代碼函數修改了外部狀態,不是純函數:
```
let greeting
const greet = (name) => {
greeting = `Hi, ${name}`
}
greet('Brianne')
greeting // "Hi, Brianne"
```
<br>
## 冪等性 (Idempotent)
冪等性是指執行無數次后還具有相同的效果,同一的參數運行一次函數應該與連續兩次結果一致。冪等性在函數式編程中與純度相關,但有不一致。
```
f(f(x)) = f(x)
```
<br>
```
Math.abs(Math.abs(10))
```
<br>
```
sort(sort(sort([2, 1])))
```
<br>
## 偏應用函數 (Partial Function)
傳遞給函數一部分參數來調用它,讓它返回一個函數去處理剩下的參數。
<br>
```
// 創建偏函數,固定一些參數
const partical = (f, ...args) =>
// 返回一個帶有剩余參數的函數
(...moreArgs) =>
// 調用原始函數
f(...args, ...moreArgs)
const add3 = (a, b, c) => a + b + c
// (...args) => add3(2, 3, ...args)
// (c) => 2 + 3 + c
const fivePlus = partical(add3, 2, 3)
fivePlus(4) // 9
```
<br>
也可以使用 Function.prototype.bind 實現偏函數。
```
const add1More = add3.bind(null, 2, 3)
```
<br>
偏函數應用通過對復雜的函數填充一部分數據來構成一個簡單的函數。柯里化通過偏函數實現。
<br>
偏函數之所以“偏”,在就在于其只能處理那些能與至少一個case語句匹配的輸入,而不能處理所有可能的輸入。
<br>
## 柯里化 (Currying)
將一個多元函數轉變為一元函數的過程。 每當函數被調用時,它僅僅接收一個參數并且返回帶有一個參數的函數,直到傳遞完所有的參數。
```
var checkage = min => (age => age > min);
var checkage18 = checkage(18);
checkage18(20);
const sum = (a, b) => a + b
const curriedSum = (a) => (b) => a + b
curriedSum(3)(4) // 7
const add2 = curriedSum(2)
add2(10) // 12
```
<br>
優點
事實上柯里化是一種“預加載”函數的方法,通過傳遞較少的參數,得到一個已經記住了這些參數的新函數,某種意義上講,這是一種對參數的“緩存”,是一種非常高效的編寫函數的方法。
<br>
## 函數組合 (Function Composing)
接收多個函數作為參數,從右到左,一個函數的輸入為另一個函數的輸出。

```
const compose = function (f, g) {
return function (x) {
return f(g(x));
};
}
var first = arr => arr[0];
var reverse = arr => arr.reverse();
var last = compose(first, reverse);
console.log(last([1,2,3,4,5])) // 5
```
<br>
函數的合成還必須滿足結合律。

```
compose(f, compose(g, h))
// 等同于
compose(compose(f, g), h)
// 等同于
compose(f, g, h)
```
<br>
## Point-Free
把一些對象自帶的方法轉化成純函數,不要命名轉瞬即逝的中間變量。
<br>
這個函數中,我們使用了 str 作為我們的中間變量,但這個中間變量除了讓代碼變得長了一點以外是毫無意義的。
<br>
```
const f = str => str.toUpperCase().split(' ');
```
<br>
使用point-free風格:
```
var toUpperCase = word => word.toUpperCase();
var split = x => (str => str.split(x));
var f = compose(split(' '), toUpperCase);
f("abcd efgh");
```
<br>
這種風格能夠幫助我們減少不必要的命名,讓代碼保持簡潔和通用。
<br>
## 高階函數 (Higher-Order Function / HOF)
函數當參數,把傳入的函數做一個封裝,然后返回這個封裝函數,達到更高程度的抽象。
```
// 命令式
var add = function(a,b){
return a + b;
};
function math(func,array){
return func(array[0],array[1]);
}
math(add,[1,2]); // 3
```
* 它是一等公民
* 它已一個函數作為參數
* 已一個函數作為返回結果
<br>
## 尾調用
### 什么是尾調用
指函數內部的最后一個動作是函數調用。該調用的返回值,直接返回給函數。
```
function f(x){
return g(x);
}
```
上面代碼中,函數f的最后一步是調用函數g,這就叫尾調用。
<br>
以下三種情況,都不屬于尾調用。
```
// 情況一
function f(x){
let y = g(x);
return y;
}
// 情況二
function f(x){
return g(x) + 1;
}
// 情況三
function f(x){
g(x);
}
```
上面代碼中,情況一是調用函數g之后,還有賦值操作,所以不屬于尾調用,即使語義完全一樣。
情況二也屬于調用后還有操作,即使寫在一行內。
情況三等同于下面的代碼。
```
function f(x){
g(x);
return undefined;
}
```
<br>
尾調用**不一定出現在函數尾部**,**只要是最后一步操作即可**。
```
function f(x) {
if (x > 0) {
return m(x)
}
return n(x);
}
```
上面代碼中,函數m和n都屬于尾調用,因為它們都是函數f的最后一步操作。
> 函數調用自身,稱為遞歸。
如果尾調用自身,就稱為尾遞歸。
遞歸需要保存大量的調用記錄,很容易發生棧溢出錯誤,如果使用尾遞歸優化,將遞歸變為循環,那么只需要保存一個調用記錄,這樣就不會發生棧溢出錯誤了。
<br>
### 尾調用優化
尾調用之所以與其他調用不同,就在于它的特殊的調用位置。
<br>
我們知道,函數調用會在內存形成一個“調用記錄”,又稱“調用幀”(call frame),保存調用位置和內部變量等信息。如果在函數A的內部調用函數B,那么在A的調用幀上方,還會形成一個B的調用幀。等到B運行結束,將結果返回到A,B的調用幀才會消失。如果函數B內部還調用函數C,那就還有一個C的調用幀,以此類推。所有的調用幀,就形成一個“調用棧”(call stack)。
<br>
**尾調用由于是函數的最后一步操作,所以不需要保留外層函數的調用幀,因為調用位置、內部變量等信息都不會再用到了,只要直接用內層函數的調用幀,取代外層函數的調用幀就可以了。**
```
function f() {
let m = 1;
let n = 2;
return g(m + n);
}
f();
// 等同于
function f() {
return g(3);
}
f();
// 等同于
g(3);
```
<br>
上面代碼中,如果函數g不是尾調用,函數f就需要保存內部變量m和n的值、g的調用位置等信息。但由于調用g之后,函數f就結束了,所以執行到最后一步,完全可以刪除f(x)的調用幀,只保留g(3)的調用幀。
<br>
這就叫做“尾調用優化”(Tail call optimization),即只保留內層函數的調用幀。**如果所有函數都是尾調用,那么完全可以做到每次執行時,調用幀只有一項,這將大大節省內存**。這就是“尾調用優化”的意義。
<br>
注意,**只有不再用到外層函數的內部變量,內層函數的調用幀才會取代外層函數的調用幀**,否則就無法進行“尾調用優化”。
<br>
```
function addOne(a){
var one = 1;
function inner(b){
return b + one;
}
return inner(a);
}
```
<br>
上面的函數不會進行尾調用優化,因為內層函數inner用到了外層函數addOne的內部變量one。
<br>
### 傳統遞歸
普通遞歸時,內存需要記錄調用的堆棧所出的深度和位置信息。在最底層計算返回值,再根據記錄的信息,跳回上一層級計算,然后再跳回更高一層,依次運行,直到最外層的調用函數。在cpu計算和內存會消耗很多,而且當深度過大時,會出現堆棧溢出。
```
function sum(n) {
if (n === 1) return 1;
return n + sum(n - 1);
}
// sum(5)
// (5 + sum(4))
// (5 + (4 + sum(3)))
// (5 + (4 + (3 + sum(2))))
// (5 + (4 + (3 + (2 + sum(1)))))
// (5 + (4 + (3 + (2 + 1))))
// (5 + (4 + (3 + 3)))
// (5 + (4 + 6))
// (5 + 10)
// 15
```
<br>
### 尾遞歸
整個計算過程是線性的,調用一次sum(x, total)后,會進入下一個棧,相關的數據信息和跟隨進入,不再放在堆棧上保存。當計算完最后的值之后,直接返回到最上層的sum(5,0)。這能有效的防止堆棧溢出。
在ECMAScript 6,我們將迎來尾遞歸優化,通過尾遞歸優化,javascript代碼在解釋成機器碼的時候,將會向while看齊,也就是說,同時擁有數學表達能力和while的效能。
```
function sum(x, total) {
if (x === 1) {
return x + total;
}
return sum(x - 1, x + total);
}
// sum(5, 0)
// sum(4, 5)
// sum(3, 9)
// sum(2, 12)
// sum(1, 14)
// 15
```
<br>
還有一個比較著名的例子,就是計算 Fibonacci 數列,也能充分說明尾遞歸優化的重要性。
<br>
非尾遞歸的 Fibonacci 數列實現如下。
```
function Fibonacci (n) {
if ( n <= 1 ) {return 1};
return Fibonacci(n - 1) + Fibonacci(n - 2);
}
Fibonacci(10) // 89
Fibonacci(100) // 超時
Fibonacci(500) // 超時
```
<br>
尾遞歸優化過的 Fibonacci 數列實現如下。
```
function Fibonacci2 (n , ac1 = 1 , ac2 = 1) {
if( n <= 1 ) {return ac2};
return Fibonacci2 (n - 1, ac2, ac1 + ac2);
}
Fibonacci2(100) // 573147844013817200000
Fibonacci2(1000) // 7.0330367711422765e+208
Fibonacci2(10000) // Infinity
```
<br>
尾遞歸的判斷標準是函數運行【最后一步】是否調用自身,而不是是否在函數的【最后一行】調用自身,最后一行調用其他函數并返回叫尾調用。
<br>
按道理尾遞歸調用調用棧永遠都是更新當前的棧幀而已,這樣就完全避免了爆棧的危險。但是現如今的瀏覽器并未完全支持。原因有二:
* 在引擎層面消除遞歸是一個隱式的行為,程序員意識不到。
* 堆棧信息丟失了 開發者難已調試
<br>
### 嚴格模式
ES6 的尾調用優化只在嚴格模式下開啟,正常模式是無效的。
這是因為在正常模式下,函數內部有兩個變量,可以跟蹤函數的調用棧。
* `func.arguments`:返回調用時函數的參數。
* `func.caller`:返回調用當前函數的那個函數。
<br>
尾調用優化發生時,函數的調用棧會改寫,因此上面兩個變量就會失真。嚴格模式禁用這兩個變量,所以尾調用模式僅在嚴格模式下生效。
~~~javascript
function restricted() {
'use strict';
restricted.caller; // 報錯
restricted.arguments; // 報錯
}
restricted();
~~~
<br>
### 尾遞歸優化的實現
尾遞歸優化只在嚴格模式下生效,那么正常模式下,或者那些不支持該功能的環境中,有沒有辦法也使用尾遞歸優化呢?回答是可以的,就是自己實現尾遞歸優化。
<br>
它的原理非常簡單。尾遞歸之所以需要優化,原因是調用棧太多,造成溢出,那么只要減少調用棧,就不會溢出。怎么做可以減少調用棧呢?就是采用“循環”換掉“遞歸”。
<br>
下面是一個正常的遞歸函數。
~~~javascript
function sum(x, y) {
if (y > 0) {
return sum(x + 1, y - 1);
} else {
return x;
}
}
sum(1, 100000)
// Uncaught RangeError: Maximum call stack size exceeded(…)
~~~
<br>
上面代碼中,`sum`是一個遞歸函數,參數`x`是需要累加的值,參數`y`控制遞歸次數。一旦指定`sum`遞歸 100000 次,就會報錯,提示超出調用棧的最大次數。
<br>
蹦床函數(trampoline)可以將遞歸執行轉為循環執行。
~~~javascript
function trampoline(f) {
while (f && f instanceof Function) {
f = f();
}
return f;
}
~~~
<br>
上面就是蹦床函數的一個實現,它接受一個函數`f`作為參數。只要`f`執行后返回一個函數,就繼續執行。注意,這里是返回一個函數,然后執行該函數,而不是函數里面調用函數,這樣就避免了遞歸執行,從而就消除了調用棧過大的問題。
<br>
然后,要做的就是將原來的遞歸函數,改寫為每一步返回另一個函數。
~~~javascript
function sum(x, y) {
if (y > 0) {
return sum.bind(null, x + 1, y - 1);
} else {
return x;
}
}
~~~
<br>
上面代碼中,`sum`函數的每次執行,都會返回自身的另一個版本。
<br>
現在,使用蹦床函數執行`sum`,就不會發生調用棧溢出。
~~~javascript
trampoline(sum(1, 100000))
// 100001
~~~
<br>
蹦床函數并不是真正的尾遞歸優化,下面的實現才是。
~~~javascript
function tco(f) {
var value;
var active = false;
var accumulated = [];
return function accumulator() {
accumulated.push(arguments);
if (!active) {
active = true;
while (accumulated.length) {
value = f.apply(this, accumulated.shift());
}
active = false;
return value;
}
};
}
var sum = tco(function(x, y) {
if (y > 0) {
return sum(x + 1, y - 1)
}
else {
return x
}
});
sum(1, 100000)
// 100001
~~~
<br>
上面代碼中,`tco`函數是尾遞歸優化的實現,它的奧妙就在于狀態變量`active`。默認情況下,這個變量是不激活的。一旦進入尾遞歸優化的過程,這個變量就激活了。然后,每一輪遞歸`sum`返回的都是`undefined`,所以就避免了遞歸執行;而`accumulated`數組存放每一輪`sum`執行的參數,總是有值的,這就保證了`accumulator`函數內部的`while`循環總是會執行。這樣就很巧妙地將“遞歸”改成了“循環”,而后一輪的參數會取代前一輪的參數,保證了調用棧只有一層。
<br>
## 函子
函數不僅可以用于同一個范疇之中值的轉換,還可以用于將一個范疇轉成另一個范疇。這就涉及到了函子(Functor)。
<br>
函子是函數式編程里面最重要的數據類型,也是基本的運算單位和功能單位。
<br>
它首先是一種范疇,也就是說,是一個容器,包含了值和變形關系。比較特殊的是,它的變形關系可以依次作用于每一個值,將當前容器變形成另一個容器。
<br>
`Functor `是一個對于函數調用的抽象,我們賦予容器自己去調用函數的能力。把東西裝進一個容器,只留出一個接口 `map `給容器外的函數,`map `一個函數時,我們讓容器自己來運行這個函數,這樣容器就可以自由地選擇何時何地如何操作這個函數,以致于擁有惰性求值、錯誤處理、異步調用等等非常的特性。
<br>
任何具有map方法的數據結構,都可以當作函子的實現。
```
class Functor {
constructor(val) {
this.val = val;
}
map(f) {
return new Functor(f(this.val));
}
}
```
<br>
上面代碼中,`Functor`是一個函子,它的`map`方法接受函數f作為參數,然后返回一個新的函子,里面包含的值是被f處理過的(`f(this.val)`)。
<br>
一般約定,函子的標志就是容器具有map方法。該方法將容器里面的每一個值,映射到另一個容器。
<br>
下面是一些用法的示例。
```
(new Functor(2)).map(function (two) {
return two + 2;
});
// Functor(4)
(new Functor('flamethrowers')).map(function(s) {
return s.toUpperCase();
});
// Functor('FLAMETHROWERS')
(new Functor('bombs')).map(_.concat(' away')).map(_.prop('length'));
// Functor(10)
```
<br>
上面的例子說明,函數式編程里面的運算,都是通過函子完成,即運算不直接針對值,而是針對這個值的容器----函子。函子本身具有對外接口(map方法),各種函數就是運算符,通過接口接入容器,引發容器里面的值的變形。
<br>
## of 方法
上面生成新的函子的時候,用了new命令。這實在太不像函數式編程了,因為new命令是面向對象編程的標志。函數式編程一般約定,函子有一個of方法,用來生成新的容器。
```
Functor.of = function(val) {
return new Functor(val);
};
```
<br>
## maybe函子
函子接受各種函數,處理容器內部的值。這里就有一個問題,容器內部的值可能是一個空值(比如null),而外部函數未必有處理空值的機制,如果傳入空值,很可能就會出錯。
<br>
Maybe 函子就是為了解決這一類問題而設計的。簡單說,它的map方法里面設置了空值檢查。
```
class Maybe {
constructor(val) {
this.val = val
}
static of(val) {
// 生成新的容器
return new Maybe(val)
}
isNothing() {
return (this.val === null || this.val === undefined)
}
map(f) {
return this.isNothing() ? Maybe.of(null) : Maybe.of(f(this.val))
}
}
```
<br>
有了 Maybe 函子,處理空值就不會出錯了。
```
Maybe.of(null).map(function (s) {
return s.toUpperCase();
});
// Maybe(null)
```
<br>
## Either 函子
條件運算if...else是最常見的運算之一,函數式編程里面,使用 Either 函子表達。
<br>
Either 函子內部有兩個值:左值(Left)和右值(Right)。右值是正常情況下使用的值,左值是右值不存在時使用的默認值。
```
class Either {
constructor(left, right) {
this.left = left
this.right = right
}
static of(left, right) {
return new Either(left, right)
}
map(f) {
return this.right ?
Either.of(this.left, f(this.right)) :
Either.of(f(this.left), this.right)
}
}
```
<br>
Either 函子的常見用途是提供默認值。下面是一個例子。
```
Either
.of({address: 'xxx'}, currentUser.address)
.map(updateField);
```
上面代碼中,如果用戶沒有提供地址,Either 函子就會使用左值的默認地址。
<br>
Either 函子的另一個用途是代替try...catch,使用左值表示錯誤。
```
class Left {
constructor(val) {
this.val = val
}
static of (x) {
return new Left(x)
}
map (f) {
return this
}
}
class Right {
constructor(val) {
this.val = val
}
static of (x) {
return new Left(x)
}
map (f) {
return Right.of(f(this.val))
}
}
var getAge = user => user.age ? Right.of(user.age) : Left.of('ERROR!');
console.log(getAge({name: 'stark', age: '21'}).map(age => 'Age is ' + age)) // Left { val: '21' }
console.log(getAge({name: 'stark'}).map(age => 'Age is ' + age)) // Left('ERROR!')
```
Left 可以讓調用鏈中任意一環的錯誤立刻返回到調用鏈的尾部,這給我們錯誤處理帶來了很大的方便,再也不用一層又一層的 try/catch。
<br>
## AP 函子
函子里面包含的值,完全可能是函數。我們可以想象這樣一種情況,一個函子的值是數值,另一個函子的值是函數。
```
class Ap {
constructor(val) {
this.val = val
}
static of(val) {
// 生成新的容器
return new Ap(val)
}
ap(F) {
return Ap.of(this.val(F.val));
}
}
function addTwo(x) {
return x + 2;
}
console.log(Ap.of(addTwo).ap(Ap.of(2)))
```
<br>
## Monad函子
Monad就是一種設計模式,表示將一個運算過程,通過函數拆解成互相連接的多個步驟。你只要提供下一步運算所需的函數,整個運算就會自動進行下去。
<br>
Promise 就是一種 Monad。Monad 讓我們避開了嵌套地獄,可以輕松地進行深度嵌套的函數式編程,比如IO和其它異步任務。
<br>
Monad 函子的作用是,總是返回一個單層的函子。它有一個flatMap方法,與map方法作用相同,唯一的區別是如果生成了一個嵌套函子,它會取出后者內部的值,保證返回的永遠是一個單層的容器,不會出現嵌套的情況。
<br>
```
class Monad extends Functor {
join() {
return this.val;
}
flatMap(f) {
return this.map(f).join();
}
}
```
上面代碼中,如果函數f返回的是一個函子,那么this.map(f)就會生成一個嵌套的函子。所以,join方法保證了flatMap方法總是返回一個單層的函子。這意味著嵌套的函子會被鋪平(flatten)。
<br>
## IO 操作
Monad 函子的重要應用,就是實現 I/O (輸入輸出)操作。
<br>
I/O 是不純的操作,普通的函數式編程沒法做,這時就需要把 IO 操作寫成Monad函子,通過它來完成。
<br>
```
var fs = require('fs');
var path = require('path')
var _ = require('lodash');
var compose = _.flowRight;
//基礎函子
class Functor {
constructor(val) {
this.val = val;
}
}
//Monad 函子
class Monad extends Functor {
join() {
return this.val;
}
flatMap(f) {
//1.f 接受一個函數返回的IO函子
//2.this.val 等于上一步的臟操作
//3.this.map(f) compose(f, this.val) 函數組合 需要手動執行
//4.返回這個組合函數并執行 注意先后的順序
return this.map(f).join();
}
}
//IO函子用來包裹臟操作
class IO extends Monad {
//val是最初的臟操作
static of(val) {
return new IO(val);
}
map(f) {
return IO.of(compose(f, this.val))
}
}
var readFile = function(filename) {
return new IO(function() {
return fs.readFileSync(filename, 'utf-8');
});
};
var print = function(x) {
return new IO(function() {
console.log(x);
return x;
});
}
```
上面代碼中,讀取文件和打印本身都是不純的操作,但是readFile和print卻是純函數,因為它們總是返回 IO 函子。
<br>
如果 IO 函子是一個Monad,具有flatMap方法,那么我們就可以像下面這樣調用這兩個函數。
```
readFile('./user.txt')
.flatMap(print)
```
<br>
這就是神奇的地方,上面的代碼完成了不純的操作,但是因為flatMap返回的還是一個 IO 函子,所以這個表達式是純的。我們通過一個純的表達式,完成帶有副作用的操作,這就是 Monad 的作用。
<br>
由于返回還是 IO 函子,所以可以實現鏈式操作。因此,在大多數庫里面,flatMap方法被改名成chain。
```
var tail = function(x) {
return new IO(function() {
return x[x.length - 1];
});
}
readFile('./user.txt')
.flatMap(tail)
.flatMap(print)
// 等同于
readFile('./user.txt')
.chain(tail)
.chain(print)
```
上面代碼讀取了文件user.txt,然后選取最后一行輸出。
<br>
<br>
# 流行的幾大函數式編程庫
* RxJS
* cycleJS
* lodashJS、lazy(惰性求值)
* underscoreJS
* ramdajs
<br>
# 實際應用場景
* 易調試、熱部署、并發
* 單元測試
<br>
<br>
# 參考資料
[函數式編程入門教程](http://www.ruanyifeng.com/blog/2017/02/fp-tutorial.html)
[函數式編程術語](https://github.com/shfshanyue/fp-jargon-zh#monad)
[函數式編程指南](https://legacy.gitbook.com/book/llh911001/mostly-adequate-guide-chinese)
[ES6入門指南 - 尾調用優化](http://es6.ruanyifeng.com/#docs/function#尾調用優化)
[Pointfree Javascript | Lucas Reis' Blog](https://lucasmreis.github.io/blog/pointfree-javascript/)
- 第一部分 HTML
- meta
- meta標簽
- HTML5
- 2.1 語義
- 2.2 通信
- 2.3 離線&存儲
- 2.4 多媒體
- 2.5 3D,圖像&效果
- 2.6 性能&集成
- 2.7 設備訪問
- SEO
- Canvas
- 壓縮圖片
- 制作圓角矩形
- 全局屬性
- 第二部分 CSS
- CSS原理
- 層疊上下文(stacking context)
- 外邊距合并
- 塊狀格式化上下文(BFC)
- 盒模型
- important
- 樣式繼承
- 層疊
- 屬性值處理流程
- 分辨率
- 視口
- CSS API
- grid(未完成)
- flex
- 選擇器
- 3D
- Matrix
- AT規則
- line-height 和 vertical-align
- CSS技術
- 居中
- 響應式布局
- 兼容性
- 移動端適配方案
- CSS應用
- CSS Modules(未完成)
- 分層
- 面向對象CSS(未完成)
- 布局
- 三列布局
- 單列等寬,其他多列自適應均勻
- 多列等高
- 圣杯布局
- 雙飛翼布局
- 瀑布流
- 1px問題
- 適配iPhoneX
- 橫屏適配
- 圖片模糊問題
- stylelint
- 第三部分 JavaScript
- JavaScript原理
- 內存空間
- 作用域
- 執行上下文棧
- 變量對象
- 作用域鏈
- this
- 類型轉換
- 閉包(未完成)
- 原型、面向對象
- class和extend
- 繼承
- new
- DOM
- Event Loop
- 垃圾回收機制
- 內存泄漏
- 數值存儲
- 連等賦值
- 基本類型
- 堆棧溢出
- JavaScriptAPI
- document.referrer
- Promise(未完成)
- Object.create
- 遍歷對象屬性
- 寬度、高度
- performance
- 位運算
- tostring( ) 與 valueOf( )方法
- JavaScript技術
- 錯誤
- 異常處理
- 存儲
- Cookie與Session
- ES6(未完成)
- Babel轉碼
- let和const命令
- 變量的解構賦值
- 字符串的擴展
- 正則的擴展
- 數值的擴展
- 數組的擴展
- 函數的擴展
- 對象的擴展
- Symbol
- Set 和 Map 數據結構
- proxy
- Reflect
- module
- AJAX
- ES5
- 嚴格模式
- JSON
- 數組方法
- 對象方法
- 函數方法
- 服務端推送(未完成)
- JavaScript應用
- 復雜判斷
- 3D 全景圖
- 重載
- 上傳(未完成)
- 上傳方式
- 文件格式
- 渲染大量數據
- 圖片裁剪
- 斐波那契數列
- 編碼
- 數組去重
- 淺拷貝、深拷貝
- instanceof
- 模擬 new
- 防抖
- 節流
- 數組扁平化
- sleep函數
- 模擬bind
- 柯里化
- 零碎知識點
- 第四部分 進階
- 計算機原理
- 數據結構(未完成)
- 算法(未完成)
- 排序算法
- 冒泡排序
- 選擇排序
- 插入排序
- 快速排序
- 搜索算法
- 動態規劃
- 二叉樹
- 瀏覽器
- 瀏覽器結構
- 瀏覽器工作原理
- HTML解析
- CSS解析
- 渲染樹構建
- 布局(Layout)
- 渲染
- 瀏覽器輸入 URL 后發生了什么
- 跨域
- 緩存機制
- reflow(回流)和repaint(重繪)
- 渲染層合并
- 編譯(未完成)
- Babel
- 設計模式(未完成)
- 函數式編程(未完成)
- 正則表達式(未完成)
- 性能
- 性能分析
- 性能指標
- 首屏加載
- 優化
- 瀏覽器層面
- HTTP層面
- 代碼層面
- 構建層面
- 移動端首屏優化
- 服務器層面
- bigpipe
- 構建工具
- Gulp
- webpack
- Webpack概念
- Webpack工具
- Webpack優化
- Webpack原理
- 實現loader
- 實現plugin
- tapable
- Webpack打包后代碼
- rollup.js
- parcel
- 模塊化
- ESM
- 安全
- XSS
- CSRF
- 點擊劫持
- 中間人攻擊
- 密碼存儲
- 測試(未完成)
- 單元測試
- E2E測試
- 框架測試
- 樣式回歸測試
- 異步測試
- 自動化測試
- PWA
- PWA官網
- web app manifest
- service worker
- app install banners
- 調試PWA
- PWA教程
- 框架
- MVVM原理
- Vue
- Vue 餓了么整理
- 樣式
- 技巧
- Vue音樂播放器
- Vue源碼
- Virtual Dom
- computed原理
- 數組綁定原理
- 雙向綁定
- nextTick
- keep-alive
- 導航守衛
- 組件通信
- React
- Diff 算法
- Fiber 原理
- batchUpdate
- React 生命周期
- Redux
- 動畫(未完成)
- 異常監控、收集(未完成)
- 數據采集
- Sentry
- 貝塞爾曲線
- 視頻
- 服務端渲染
- 服務端渲染的利與弊
- Vue SSR
- React SSR
- 客戶端
- 離線包
- 第五部分 網絡
- 五層協議
- TCP
- UDP
- HTTP
- 方法
- 首部
- 狀態碼
- 持久連接
- TLS
- content-type
- Redirect
- CSP
- 請求流程
- HTTP/2 及 HTTP/3
- CDN
- DNS
- HTTPDNS
- 第六部分 服務端
- Linux
- Linux命令
- 權限
- XAMPP
- Node.js
- 安裝
- Node模塊化
- 設置環境變量
- Node的event loop
- 進程
- 全局對象
- 異步IO與事件驅動
- 文件系統
- Node錯誤處理
- koa
- koa-compose
- koa-router
- Nginx
- Nginx配置文件
- 代理服務
- 負載均衡
- 獲取用戶IP
- 解決跨域
- 適配PC與移動環境
- 簡單的訪問限制
- 頁面內容修改
- 圖片處理
- 合并請求
- PM2
- MongoDB
- MySQL
- 常用MySql命令
- 自動化(未完成)
- docker
- 創建CLI
- 持續集成
- 持續交付
- 持續部署
- Jenkins
- 部署與發布
- 遠程登錄服務器
- 增強服務器安全等級
- 搭建 Nodejs 生產環境
- 配置 Nginx 實現反向代理
- 管理域名解析
- 配置 PM2 一鍵部署
- 發布上線
- 部署HTTPS
- Node 應用
- 爬蟲(未完成)
- 例子
- 反爬蟲
- 中間件
- body-parser
- connect-redis
- cookie-parser
- cors
- csurf
- express-session
- helmet
- ioredis
- log4js(未完成)
- uuid
- errorhandler
- nodeclub源碼
- app.js
- config.js
- 消息隊列
- RPC
- 性能優化
- 第七部分 總結
- Web服務器
- 目錄結構
- 依賴
- 功能
- 代碼片段
- 整理
- 知識清單、博客
- 項目、組件、庫
- Node代碼
- 面試必考
- 91算法
- 第八部分 工作代碼總結
- 樣式代碼
- 框架代碼
- 組件代碼
- 功能代碼
- 通用代碼