特點
1.函數是一等公民
2.只用表達式不用語句
3.沒有副作用(side effect)
4.不修改狀態
5.引用透明
優勢
1. 代碼簡潔,開發快速
2. 接近自然語言,易於理解
3. 更方便的代碼管理
4. 易於"並發編程"
5. 代碼的熱升級
范疇與容器
我們可以把"范疇"想象成是一個容器,里面包含兩樣東西。
class Category{
constructor(val){
this.val = val
}
addOne(x){
return x + 1
}
}
函數的合成與柯里化
X和Y之間的變形關系是函數f,Y和Z之間的變形關系是函數g,那么X和Z之間的關系,就是g和f的合成函數g·f。
const compose = (f,g) => {
return function(x) {
return f(g(x))
}
}
滿足結合律
compose(f, compose(g, h))
// 等同於
compose(compose(f, g), h)
// 等同於
compose(f, g, h)
柯里化
有了柯里化以后,我們就能做到,所有函數只接受一個參數
f(x)和g(x)合成為f(g(x)),有一個隱藏的前提,就是f和g都只能接受一個參數。如果可以接受多個參數,比如f(x, y)和g(a, b, c),函數合成就非常麻煩。
//柯里化之前
function add(x,y){
return x + y;
}
add(1,2)
//柯里化之后
function addX(y){
return function(x){
return x + y
}
}
addX(2)(1)
函子
函子是函數式編程里面最重要的數據類型,也是基本的運算單位和功能單位。
左側的圓圈就是一個函子,表示人名的范疇。外部傳入函數f,會轉成右邊表示早餐的范疇。
任何具有map方法的數據結構,都可以當作函子的實現。
class Functor {
constructor(val){
this.val = val
}
map(f){
return new Functor(f(this.val))
}
}
上面代碼中,Functor是一個函子,它的map方法接受函數f作為參數,然后返回一個新的函子,里面包含的值是被f處理過的(f(this.val))。
一般約定,函子的標志就是容器具有map方法。該方法將容器里面的每一個值,映射到另一個容器。
(new Functor(2)).map(function(two){
return two + 2
}
(new Functor('plus')).map(function(s){
return s.toUpperCase()
})
(new Functor('bombs')).map(_.concat(' away')).map(_.prop('length'));
上面的例子說明,函數式編程里面的運算,都是通過函子完成,即運算不直接針對值,而是針對這個值的容器----函子。函子本身具有對外接口(map方法),各種函數就是運算符,通過接口接入容器,引發容器里面的值的變形。
因此,學習函數式編程,實際上就是學習函子的各種運算。由於可以把運算方法封裝在函子里面,所以又衍生出各種不同類型的函子,有多少種運算,就有多少種函子。函數式編程就變成了運用不同的函子,解決實際問題。
of方法
函數式編程一般約定,函子有一個of方法,用來生成新的容器。
Functor.of = function(val){
return new Functor(val)
}
Functor.of(2).map(function(two){
return two + 2
})
Maybe函子
函子接受各種函數,處理容器內部的值。這里就有一個問題,容器內部的值可能是一個空值(比如null),而外部函數未必有處理空值的機制,如果傳入空值,很可能就會出錯。
//報錯
Functor.of(null).map(function(s){
return s.toUpperCase()
})
//解決
class Maybe extends Functor{
map(f){
return this.val ? Maybe.of(f(this.val)) : Maybe.of(null)
}
}
Maybe.of(null).map(function(s){
return s.toUpperCase()
})
Either函子
條件運算if...else是最常見的運算之一, 函數式編程里面,使用 Either 函子表達。
Either 函子內部有兩個值:左值(Left)和右值(Right)。右值是正常情況下使用的值,左值是右值不存在時使用的默認值。
class Either extends Functor{
constructor(left,right){
this.left = left;
this.right = right;
}
map(f){
return this.right ? Either.of(this.left,f(this.right)) :
Either.of(f(this.left),this.right)
}
}
Either.of = function(left,right){
return new Either(left,right)
}
var addOne = function(x){
return x + 1
}
Either.of(5,6).map(addOne)
Either.of(1,null).map(addOne)
上面代碼中,如果右值有值,就使用右值,否則使用左值。通過這種方式,Either 函子表達了條件運算。
Either 函子的常見用途是提供默認值。下面是一個例子。
Either.of({address:'xxx'},currentUser.address).map(updateField)
上面代碼中,如果用戶沒有提供地址,Either 函子就會使用左值的默認地址。
Either函子的另一個用途是代替try...catch,使用左值表示錯誤
fucntion parseJSON(json){
try{
return Either.of(null,JSON.parse(json))
} catch(e:Error){
return Either.of(e,null)
}
}
上面代碼中,左值為空,就表示沒有出錯,否則左值會包含一個錯誤對象e。一般來說,所有可能出錯的運算,都可以返回一個 Either 函子。
ap函子
函子里面包含的值,完全可能是函數。我們可以想象這樣一種情況,一個函子的值是數值,另一個函子的值是函數。
function addTwo(x){
return x + 2
}
const A = Functor.of(2)
const B = Functor.of(addTwo)
上面代碼中,函子A內部的值是2,函子B內部的值是函數addTwo。
有時,我們想讓函子B內部的函數,可以使用函子A內部的值進行運算。這時就需要用到 ap 函子。
ap 是 applicative(應用)的縮寫。凡是部署了ap方法的函子,就是 ap 函子。
class Ap extends Functor {
ap(F){
return Ap.of(this.val(F.val))
}
}
注意,ap方法的參數不是函數,而是另一個函子。
因此,前面例子可以寫成下面的形式。
Ap.of(addTwo).ap(Functor.of(2))
ap 函子的意義在於,對於那些多參數的函數,就可以從多個容器之中取值,實現函子的鏈式操作。
function add(x){
return function(y){
return x + y
}
}
Ap.of(add).ap(Maybe.of(2)).ap(Maybe.of(3))
Monad函子
函子是一個容器,可以包含任何值。函子之中再包含一個函子,也是完全合法的。但是,這樣就會出現多層嵌套的函子。
Monad 函子的作用是,總是返回一個單層的函子。它有一個flatMap方法,與map方法作用相同,唯一的區別是如果生成了一個嵌套函子,它會取出后者內部的值,保證返回的永遠是一個單層的容器,不會出現嵌套的情況。
Maybe.of(
Maybe.of(
Maybe.of({
name:'plus',
number:88888888
})
)
)
class Monad extends Functor{
join(){
return this.val
}
flatMap(f){
return this.map(f).join()
}
}
IO操作
I/O 是不純的操作,普通的函數式編程沒法做,這時就需要把 IO 操作寫成Monad函子,通過它來完成。
var fs = require('fs')
var readFile = function(filename){
return new IO(function(){
return fs.readFileSync(filename,'utf8')
})
}
var print = function(x){
return new IO(function(){
console.log(x);
return x
})
}
上面代碼中,讀取文件和打印本身都是不純的操作,但是readFile和print卻是純函數,因為它們總是返回 IO 函子。
如果 IO 函子是一個Monad,具有flatMap方法,那么我們就可以像下面這樣調用這兩個函數。
readFile('xxx').flatMap(print)
這就是神奇的地方,上面的代碼完成了不純的操作,但是因為flatMap返回的還是一個 IO 函子,所以這個表達式是純的。我們通過一個純的表達式,完成帶有副作用的操作,這就是 Monad 的作用。
由於返回還是 IO 函子,所以可以實現鏈式操作。因此,在大多數庫里面,flatMap方法被改名成chain。
var tail = function(x){
return new IO(function(){
return x[x.length-1]
})
}
readFile('xxx').flatMap(tail).flatMap(print)
//等同於
readFile('xxx').chain(tail).chain(print)
實戰
class Functor {
constructor(val){
this.val = val
}
map(f){
return new Functor(f(this.val))
}
static of(val){
return new Functor(val)
}
}
const functor1 = Functor.of(2).map(two => two + 2)
const functor2 = Functor.of('plus').map(name => name.toUpperCase())
//console.log(functor2.val)
//const err = Functor.of(null).map(err => err.toUpperCase())
class Maybe extends Functor {
map(f){
return this.val ? Maybe.of(f(this.val)) : Maybe.of(this.val)
}
static of(val){
return new Maybe(val)
}
}
// const err = Maybe.of(null).map(err => err.toUpperCase())
class Either extends Functor {
constructor(left,right){
super()
this.left = left
this.right = right
}
map(f){
return this.right ? Either.of(this.left,f(this.right)) : Either.of(f(this.left),this.right)
}
}
Either.of = function(left,right){
return new Either(left,right)
}
const addOne = x => x + 1
const res = Either.of(5,6).map(addOne)
const res2 = Either.of(1,null).map(addOne)
class Ap extends Functor{
ap(F){
return Ap.of(this.val(F.val))
}
static of(val){
return new Ap(val)
}
}
const addTwo = x => x + 2
const add = x => y => x + y
const res3 = Ap.of(addTwo).ap(Functor.of(2))
const res6 = Ap.of(add).ap(Maybe.of(2)).ap(Maybe.of(3))
console.log(res6)