函數(shù)式編程

1 文章目標(biāo)

  • 為什么要學(xué)習(xí)函數(shù)式編程以及什么是函數(shù)式編程
  • 函數(shù)式編程的特性(純函數(shù)、柯里化、函數(shù)組合等)
  • 函數(shù)式編程的應(yīng)用場景
  • 函數(shù)式編程庫Lodash

2 什么是函數(shù)式編程

阮一峰老師的函數(shù)式編程入門教程:http://www.ruanyifeng.com/blog/2017/02/fp-tutorial.html
Franklin Risby 教授的函數(shù)式編程指北:https://llh911001.gitbooks.io/mostly-adequate-guide-chinese/content/ch1.html
關(guān)于什么是函數(shù)式編程,就不多說什么了,給兩個大神的鏈接給各位朋友瞅瞅。以下記錄以下函數(shù)式編程中重要的知識點

3 閉包

函數(shù)和對其周圍狀態(tài)(lexical environment,詞法環(huán)境)的引用捆綁在一起構(gòu)成閉包(closure)。也就是說,閉包可以讓你從內(nèi)部函數(shù)訪問外部函數(shù)作用域。在 JavaScript 中,每當(dāng)函數(shù)被創(chuàng)建,就會在函數(shù)生成時生成閉包。(MDN對于閉包的定義https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Closures)

通過一個只執(zhí)行一次的函數(shù)的例子,了解一下閉包的使用方式

function once(){
    let done = false;
    return function(){
        if(!done){
            console.log(done);
            done = true;
        }
    }
}

let f = once();
f()
f()
...

上面這個函數(shù),無論調(diào)用多少次,只有打印第一次。 f引用的是once內(nèi)部的函數(shù)。在外面,我們通過調(diào)用f可以訪問到once函數(shù)的作用域。

4 純函數(shù)

  • 對于相同的輸入,永遠(yuǎn)得到相同的輸出。它不依賴于程序執(zhí)行期間函數(shù)外部任何狀態(tài)或數(shù)據(jù)的變化,只依賴于輸入?yún)?shù)
  • 除了純函數(shù)以外的任何變動,都不影響純函數(shù)
  • 純函數(shù)還使得維護(hù)和重構(gòu)代碼變得更加容易,你可以放心的修改某個純函數(shù),不必關(guān)心改動會影響其它地方
  • 由于對于相同的輸入,永遠(yuǎn)得到相同的輸出,所以純函數(shù)可以緩存,之后調(diào)用傳入相同參數(shù)是,不用執(zhí)行,直接獲取之前計算的值
    純函數(shù)緩存例子
function memorize(fn){
    let caches = {} // 用于緩存之前的計算
    return function(){
        let arg_str = JSON.stringify(arguments)
        caches[arg_str] = caches[arg_str] || fn.apply(null,arguments);
        return caches[arg_str]
    }
}

function sum(a ,b){
    console.log(a,b); // 從這里可以看出執(zhí)行了幾次sum函數(shù)
    return a + b;
}

let sumM = memorize(sum)

console.log(sumM(1,2));
console.log(sumM(2,2));
console.log(sumM(1,2));

5 柯里化

  • 把一個多參數(shù)的函數(shù),轉(zhuǎn)化為單參數(shù)函數(shù)。
  • 柯里化可以讓我們給一個函數(shù)傳遞較少的參數(shù)得到一個已經(jīng)記住了某些固定參數(shù)的新函數(shù)
  • 這是一種對函數(shù)參數(shù)的'緩存'
  • 讓函數(shù)變得更靈活,讓函數(shù)的粒度更小
  • 可以把多元函數(shù)轉(zhuǎn)換成一元函數(shù),可以組合使用函數(shù)產(chǎn)生強大的功能
    這里看到一位朋友寫的關(guān)于柯里化的也不錯:http://www.itdecent.cn/p/2975c25e4d71
    下面是對于柯里化的使用例子
function curry(fn){
    return function curried(...args){
        // args還用來保存上一步的參數(shù)
        if(fn.length > args.length){
            return function(){
                return curried(...args.concat(Array.from(arguments)))
            }
        }
        return fn(...args)
    }
}

function add(a, b, c){
    return a + b + c
}

let cAdd = curry(add)

console.log(cAdd(1)(2)(3));
console.log(cAdd(1,2)(3));
console.log(cAdd(1,2,3));
console.log(cAdd(1)(2,3));

6 compose組合

WechatIMG81.png

如圖,現(xiàn)在有這么一個操作,數(shù)據(jù)a經(jīng)過f函數(shù)處理后在經(jīng)過g函數(shù)處理后得到c;代碼操作入下

function f(x){
    return x + 1
}
function g(x){
    return x * x
}
console.log(g(f(2)));

如果增加一些操作就會形如以下a(b(c(d(e())))); 為了處理這樣的函數(shù),就需要組合一下函數(shù)了,使我們最后能夠使用f(x)就能得到結(jié)果

function compose(...args){
    return function(x){
         return args.reduce(function(total,fn){
            return fn(total)
        },x)
    }
}
let p = console(f,g)

簡化compose

let compose = (...args) => x => args.reduce((total,fn) => fn(total), x);

7 函子

有些副作用是不可避免的,但是使用函子,可以將副作用控制在可控范圍內(nèi)。

7-1 什么是副作用

函數(shù)副作用是指當(dāng)調(diào)用函數(shù)時,除了返回函數(shù)值之外,還對主調(diào)用函數(shù)產(chǎn)生附加的影響。副作用的函數(shù)不僅僅只是返回了一個值,而且還做了其他的事情。這里有一邊關(guān)于副作用的文章:http://www.fly63.com/article/detial/1176

副作用如下
1、修改了一個變量
2、直接修改數(shù)據(jù)結(jié)構(gòu)
3、設(shè)置一個對象的成員
4、拋出一個異?;蛞砸粋€錯誤終止
5、打印到終端或讀取用戶輸入
6、讀取或?qū)懭胍粋€文件
7、在屏幕上畫圖

7-2 什么是函子
  • 是一個特殊的容器,通過一個普通對象來實現(xiàn),該對象具有map方法,map方法可以運行一個函數(shù)對值進(jìn)行處理(變形關(guān)系)
  • 使用函子可以實現(xiàn)鏈?zhǔn)骄幊?br> 這個例子只提到關(guān)于鏈?zhǔn)骄幊蹋?以下都是對于函子鎖引出的問題進(jìn)行解決(將副作用控制的可控范圍內(nèi))
class Functor {
    // 為了使用這個函子的時候可以不在外部顯示的使用new functor,添加一個靜態(tài)的of方法
    static of(value){
        return new Functor(value)
    }
    constructor(value){
        this._value = value
    }
    map(fn){
        return Functor.of(fn(this._value))
    }
    getVal(){
        return this._value
    }
}

let p = Functor.of(2).map(x => x + 2).map(x => x * 2).getVal()
console.log(p);
7-3 總結(jié)
  • 函數(shù)式編程的運算不直接操作值,而是由函子完成
  • 函子就是一個實現(xiàn)了map契約的對象
  • 我們可以把函子想象成一個盒子,這個盒子里封裝了一個值
  • 想要處理盒子中的值,我們需要給盒子的map方法穿第一個處理值的函數(shù)(純函數(shù)),由這個函數(shù)來對值進(jìn)行處理
  • 最終map方法返回一個包含新值的盒子(函子)

8 MayBe函子

  • 我們在編程過程中可能會遇到很多錯誤,需要對這些錯誤進(jìn)行相應(yīng)的處理
  • MayBe函子的作用就是可以對外部的空值進(jìn)行處理(控制副作用在允許范圍內(nèi))
// 由于傳入為空,不能執(zhí)行轉(zhuǎn)為大寫操作,報錯
Functor.of(null).map(x => x.toUpperCase())
class MayBe extends Functor{
    static of(value){
        return new MayBe(value)
    }
    map(fn){
        return this._value ? Functor.of(fn(this._value)) : Functor.of(null)
    }
}

let p2 = MayBe.of(null).map(x => x.toUpperCase()).getVal()
console.log(p2);

9 Either

Either 并不僅僅只對合法性檢查這種一般性的錯誤作用非凡,對一些更嚴(yán)重的、能夠中斷程序執(zhí)行的錯誤比如文件丟失或者 socket 連接斷開等,Either 同樣效果顯著。這里,我僅僅是把 Either 當(dāng)作一個錯誤消息的容器介紹給你!

class Left{
   static of(value){
       return new Left(value)
   }
   constructor(value){
       this._value = value
   }
   map(fn){
       return this
   }
}
class Right{
   static of(value){
       return new Right(value)
   }
   constructor(value){
       this._value = value
   }
   map(fn){
       return Right.of(fn(this._value))
   }
}

function parseJSON(str){
   try{
       return Right.of(JSON.parse(str))
   }catch(err){
       return Left.of({message: err.message})
   }
}

let p = parseJSON('hello world')
console.log(p);

10 IO函子

  • IO函子中的_value是一個函數(shù),這里是把函數(shù)最為值來處理
  • IO函子可以把不純的動作存儲到_value中,延遲執(zhí)行這個不純的操作(惰性執(zhí)行),包裝當(dāng)前的操作為純
  • 把不純的操作交給調(diào)用者處理
const fp = require('lodash/fp')

class IO{
    static of(value){
        return new IO(function(){
            return value
        })
    }
    constructor(fn){
        this._value = fn
    }
    map(fn){
        return new IO(fp.flowRight(fn,this._value))
    }
}

let f = new IO(process).map(p => p.execPath)

11 Folktale

  • 異步任務(wù)的實現(xiàn)過于復(fù)雜,使用folktale中的task來演示
  • folktale是一個標(biāo)準(zhǔn)的函數(shù)式編程庫
  • 和lodash、ramda、不同的是,他沒有提供很多功能函數(shù)
  • 只是提供了一些函數(shù)式處理的操作,例如:compose、curry等,一些函子 Task、Either、MayBe 等
    使用task函子執(zhí)行異步任務(wù)
// npm i folktale

// Task 處理異步任務(wù)
const fs = require('fs')
const {task} = require('folktale/concurrency/task')
const {split, find} = require('lodash/fp')

function readFile(filename){
    return task(resolver => {
        fs.readFile(filename,'utf-8',(err,data) => {
            if(err)resolver.reject(err)
            resolver.resolve(data)
        })
    })
}

readFile('package.json') // 返回task函子
.map(split('\n'))
.map(find(x => x.includes('version')))
.run()
.listen({
    onRejected: err => {
        console.log(err);
    },
    onResolved: value => {
        console.log(value);
    }
})

12 IO函子的問題

函子嵌套了

const fs = require('fs')
const fp = require('lodash/fp')

class IO{
    static of(value){
        return new IO(function(){
            return value
        })
    }
    constructor(fn){
        this._value = fn
    }
    map(fn){
        return new IO(fp.flowRight(fn,this._value))
    }
}

let readFile = function(filename){
    return new IO(function(){
        return fs.readFileSync(filename,'utf-8')
    })
}

let Print = function(x){
    return new IO(function(){
        console.log(x);
        return x
    })
}

let cat = fp.flowRight(Print, readFile)
let r = cat('package.json')
console.log(r._value()._value());

13 Monad函子

解決函子嵌套的問題

  • Monad 函子是可以變扁的 Pointed(有靜態(tài) of方法的) 函子, IO(IO(x))
  • 一個函子如果具有join和of兩個方法并遵守一些定律就是一個Monad
const fs = require('fs')
const fp = require('lodash/fp')

class IO{
    static of(value){
        return new IO(function(){
            return value
        })
    }
    constructor(fn){
        this._value = fn
    }
    map(fn){
        return new IO(fp.flowRight(fn,this._value))
    }
    join(){
        return this._value()
    }
    flatMap(fn){
        let s = this.map(fn).join()
        console.log(1,s);
        return s
    }
}

let readFile = function(filename){
    return new IO(function(){
        let file = fs.readFileSync(filename,'utf-8')
        console.log(file);
        return file
    })
}

let Print = function(x){
    console.log("flatMap中join:執(zhí)行讀取,并且執(zhí)行打印,結(jié)束后就是讀取完數(shù)據(jù),并且返回打印中那個函子")
    console.log(x);
    return new IO(function(){
        return x
    })
}

let cat = readFile('package.json')
.flatMap(Print)

console.log(cat);
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容