函數(shù)式編程——學(xué)習(xí)筆記

函數(shù)式編程中的函數(shù)指的不是程序中的函數(shù)方法,而是數(shù)學(xué)中的函數(shù)即映射關(guān)系,是對(duì)運(yùn)算過程的抽象,是用來描述數(shù)據(jù)之間的映射

// 非函數(shù)式
let a = 1, b = 2, c = a + b;
console.log(c)

// 函數(shù)式
const aa = (a, b) => {
    return a + b
}
let c = aa(1, 2)
console.log(c)

函數(shù)式編程語言的特性

函數(shù)是一等公民

  1. 函數(shù)可以存儲(chǔ)在變量中
  2. 函數(shù)可以作為參數(shù)
  3. 函數(shù)可以作為返回值

高階函數(shù)

什么是高階函數(shù)?
  1. 函數(shù)可以作為參數(shù)傳遞給另一個(gè)函數(shù)
  2. 函數(shù)可以作為另一個(gè)函數(shù)的返回結(jié)果
函數(shù)作為參數(shù)
// 模擬forEach,打印數(shù)組中的每一項(xiàng)
const forEach = (arr, fn) => {
    for (let i = 0; i < arr.length; i++) {
        fn(arr[i])
    }
}
let c = [1, 2, 3, 4, 5];
forEach(arr, (item) => {
    console.log(item)
})

// 模擬filter,把滿足條件的每一項(xiàng)存儲(chǔ)下來并返回
const filter = (arr, fn) => {
    let result = [];
    for (let i = 0; i < arr.length; i++) {
        if (fn(arr[i])) {
            result.push(arr[i])
        }
    }
    return result
}

// 測(cè)試
let arr = [1, 2, 4, 7, 8];
filter(arr, (item) => {
    return item % 2 === 0
})
函數(shù)作為返回值
const makeFn = () => {
    let msg = 'hello function';
    return () => {
        console.log(msg)
    }
}
makeFn()();

// 模擬lodash中的once,只執(zhí)行一次
const once = (fn) => {
    let done = false;
    return function () {
        if (!done) {
            done = true;
            return fn.apply(this, arguments);
        }
    }
}

let pay = once((money) => {
    console.log('gg', money)
    console.log(`支付了${money}RMB`)
});

pay(9);

使用高階函數(shù)的意義

高階函數(shù)是用來抽象通用問題,抽象可以幫我們屏蔽細(xì)節(jié),我們只用關(guān)注實(shí)現(xiàn)的目標(biāo)

// 模擬常用的高階函數(shù):map、every、some
// map 遍歷數(shù)組中的每一項(xiàng),將滿足條件的項(xiàng)存入新的數(shù)組并返回
const map = (array, fn) => {
    let result = [];
    for (let value of array) {
        result.push(fn(value))
    }
    return result
}

// 測(cè)試
console.log(map([1, 2, 3, 4], v => v * v))

// every 檢測(cè)數(shù)組所有元素是否都符合指定條件,有一項(xiàng)不滿足條件就返回false,剩余的元素不會(huì)再進(jìn)行檢測(cè)。
const every = (array, fn) => {
    let result = true;
    for (let value of array) {
        if (!fn(value)) {
            result = false;
            break;
        }
    }
    return result
}
// 測(cè)試
console.log(every([5, 7, 6,], v => v > 10));

// some 檢測(cè)數(shù)組中的元素是否滿足指定條件,如果有一個(gè)元素滿足指定條件就返回true,剩余的元素不會(huì)再繼續(xù)檢測(cè)。
const some = (array, fn) => {
    let result = true;
    for (let value of array) {
        if (fn(value)) {
            result = true;
        }
    }
    return result
}
// 測(cè)試
console.log(some([5, 8, 9, 3], (v) => v > 2));

閉包

含義:

  1. 函數(shù)和其周圍的狀態(tài)(語法環(huán)境)的引用捆綁在一起形成閉包。

  2. 可以在另一個(gè)作用域中調(diào)用一個(gè)函數(shù)的內(nèi)部函數(shù)并訪問到該函數(shù)的作用域中的成員

本質(zhì):函數(shù)在執(zhí)行的時(shí)候會(huì)放到一個(gè)執(zhí)行棧上,當(dāng)函數(shù)執(zhí)行完畢會(huì)從執(zhí)行棧上移除,但是堆上的作用于成員因?yàn)楸煌獠恳貌荒茚尫?,因此?nèi)部函數(shù)依然可以訪問外部函數(shù)的成員。

function makePower(power) {
   return function (number) {
       return Math.pow(number, power)
   }
}

// 求平方
let power2 = makePower(2);
// 求立方
let power3 = makePower(3);

console.log(power2(2))
console.log(power2(3))
console.log(power3(4))

純函數(shù)

純函數(shù)的概念

相同的輸入永遠(yuǎn)會(huì)得到相同的輸出,而且沒有任何可觀察的副作用

  1. 純函數(shù)就類似數(shù)學(xué)中的函數(shù),用來描述輸入和輸出的關(guān)系

  2. lodash是一個(gè)純函數(shù)的功能庫(kù),提供了對(duì)數(shù)組,數(shù)字,對(duì)象,字符串,函數(shù)等操作方法

  3. 數(shù)組的slice和splic分別是純函數(shù)和不純的函數(shù)

    slice返回?cái)?shù)組中的指定部分,不會(huì)改變?cè)瓟?shù)組

    splice對(duì)數(shù)組進(jìn)行操作返回該數(shù)組,會(huì)改變?cè)瓟?shù)組

let numbers = [1, 2, 3, 4, 5] 
//純函數(shù) 
numbers.slice(0, 3) // => [1, 2, 3] 
numbers.slice(0, 3) // => [1, 2, 3] 
numbers.slice(0, 3) // => [1, 2, 3] 
    
// 不純的函數(shù) 
numbers.splice(0, 3) // => [1, 2, 3] 
numbers.splice(0, 3) // => [4, 5] 
numbers.splice(0, 3) // => []

純函數(shù)代表:lodash

純函數(shù)的好處:

1、可緩存,因?yàn)榧兒瘜?duì)于相同輸入始終具有相同輸出,所以可以把純函數(shù)的結(jié)果緩存起來

2、可測(cè)試,純函數(shù)讓測(cè)試更方便

3、并行處理,在多線程環(huán)境下并行操作共享的內(nèi)存數(shù)據(jù)很有可能出現(xiàn)意外的情況;純函數(shù)只依賴參數(shù),不需要訪問共享的內(nèi)存數(shù)據(jù),所以在并行環(huán)境下可以任意運(yùn)行純函數(shù)(web worker)

函數(shù)的副作用:

// 不純的 
let mini = 18
function checkAge(age) {
    return age >= mini
}
// 純的(有硬編碼,后續(xù)可以通過柯里化解決) 
function checkAge(age) {
    let mini = 18
    return age >= mini
}

副作用讓一個(gè)純函數(shù)變的不純,如上例,純函數(shù)根據(jù)相同的輸入返回相同的輸出,如果函數(shù)依賴于外部的狀態(tài)就無法保證輸出相同,就會(huì)帶來副作用

副作用的來源:配置文件、數(shù)據(jù)庫(kù)、獲取用戶的輸入...

所有的外部交互都有可能帶來副作用,副作用也使得方法通用性下降不適合擴(kuò)展和重用性,同時(shí)副作用會(huì)給程序帶來安全隱患給程序帶來不確定性,但是副作用不可能完全禁止,盡可能控制它們?cè)诳煽胤秶鷥?nèi)發(fā)生。

柯里化

當(dāng)一個(gè)函數(shù)有多個(gè)參數(shù)的時(shí)候先傳遞一部分參數(shù)調(diào)用它(這部分參數(shù)以后永遠(yuǎn)不變),然后返回一個(gè)新的函數(shù)接收剩余的參數(shù),返回結(jié)果

lodash中的柯里化

// 柯里化案例
''.match(/\s+/g);// 提取字符串中的空白字符
''.match(/\d+/g);// 提取字符串中的數(shù)字
const _ = require('lodash');

const match = _.curry((reg, str) => str.match(reg));

const haveSpace = match(/\s+/g);
const haveNumber = match(/\d+/g);

// console.log(haveSpace('hello  word'))
// console.log(haveNumber('333adg'))

// 操作數(shù)組

const filter = _.curry((func, array) => array.filter(func))

// 獲取數(shù)組中具有空白字符的元素
const findSpace = filter(haveSpace);

console.log(findSpace(['fsf,vvv ,kkk  l']));

柯里化實(shí)現(xiàn)原理


// 模擬柯里化實(shí)現(xiàn)原理

const getSum = (a, b, c) => a + b + c;

// 模擬lodash中curry方法
const curry = (func) => {
    return function curriedFn(...args) {
        // 判斷形參和實(shí)參的個(gè)數(shù)
        if (args.length < func.length) {
            return function () {
                return curriedFn(...args.concat(Array.from(arguments)))
            }
        }
        return func(...args)
    }
}
const curried = curry(getSum);
console.log(curried(1)(2, 3)); // 6
console.log(curried(1, 2)(3));// 6
console.log(curried(1, 2, 3));// 6
總結(jié)

柯里化可以讓我們給一個(gè)函數(shù)傳遞較少的參數(shù)得到一個(gè)已經(jīng)記住啦某些固定參數(shù)的新函數(shù)

這是一種對(duì)函數(shù)參數(shù)的緩存

讓函數(shù)變得更靈活,讓函數(shù)的粒度更小

可以把多元函數(shù)轉(zhuǎn)成一元函數(shù),可以組合使用函數(shù)產(chǎn)生強(qiáng)大的功能

函數(shù)組合

如果一個(gè)函數(shù)需要經(jīng)過多個(gè)函數(shù)處理才能得到最終只,這個(gè)時(shí)候可以把中間過程的函數(shù)組合成一個(gè)函數(shù)

// 函數(shù)組合
const compose = (f, g) => {
    return function (value) {
        return f(g(value))
    }
}

// 反轉(zhuǎn)數(shù)組
const reverse = (array) => {
    return array.reverse()
}
// 獲取數(shù)組的第一個(gè)元素
const first = (array) => {
    return array[0]
}

const last = compose(first, reverse);

console.log(last([1, 2, 3, 4, 5, 6,]));

lodash中的組合函數(shù)

// 模擬lodash中的flowRight
// const _ = require('lodash');
const reverse = arr => arr.reverse();
const first = arr => arr[0];
const toUpper = s => s.toUpperCase();

// const compose = (...args) => {
//     return (value) => {
//         return args.reverse().reduce((acc, fn) => {
//             return fn(acc)
//         }, value)
//     }
// }
// ES6
const compose = (...args) => value => args.reverse().reduce((acc, fn) => fn(acc), value)

const f = compose(toUpper, first, reverse);
console.log(f(['one', 'two', 'three']));

函數(shù)結(jié)合律

// 函數(shù)結(jié)合需要滿足結(jié)合律
const _ = require('lodash');

const f = _.flowRight(_.toUpper, flowRight(_.first, _.reverse));
console.log(f(['one', 'two', 'three']));

函數(shù)組合調(diào)試

// 函數(shù)結(jié)合 調(diào)試
// NEVER SAY DIE --->never-say-die
const _ = require('lodash');

const trace = _.curry((tag, v) => {
    console.log(tag, v)
    return v
})

const split = _.curry((sep, str) => _.split(str, sep))

const join = _.curry((sep, array) => _.join(array, sep))
const map = _.curry((fn, array) => _.map(array, fn));

const f = _.flowRight(join('-'), trace('map之后'), map(_.toLower),trace('split之后'), split(' '));


console.log(f('NEVER SAY DIE'))

lodash模塊數(shù)據(jù)優(yōu)先,函數(shù)置后

lodash/fp模塊函數(shù)優(yōu)先,數(shù)據(jù)置后

// lodash和lodash/fp模塊中map方法的區(qū)別
const _ = require('lodash');
console.log(_map(["23", "8", "10"]), parseInt);
// parseInt("23",0,array)
// parseInt("8",1,array)
// parseInt("10",2,array)

const fp = require('lodash/fp');
console.log(fp.map(parseInt, ["23", "8", "10"]))

PointFree

我們可以把數(shù)據(jù)處理的過程定義成與數(shù)據(jù)無關(guān)的合成運(yùn)算,不需要用到代表數(shù)據(jù)的那個(gè)參
數(shù),只要把簡(jiǎn)單的運(yùn)算步驟合成到一起,在使用這種模式之前我們需要定義一些輔助的基本運(yùn)算函數(shù)。

1、不需要指明處理的數(shù)據(jù)

2、只需要合并運(yùn)算過程

3、需要定義一些輔助的基本運(yùn)算函數(shù)

const f = fp.flowRight(fp.join('-'), trace('map之后'), fp.map(fp.toLower), trace('split之后'), fp.split(' '));

案例

// 非pointFree模式
// const f = (word) => word.toLowerCase().replace(/\s+/g, '_');

// pointFree模式
const fp = require('lodash/fp')
const f = fp.flowRight(fp.replace(/\s+/g, "_"), fp.toLower);
console.log(f('hello   word'))
//把一個(gè)字符串的首字符提取并轉(zhuǎn)換成大寫,使用. 作為分隔符
// word wild web ==>W. W. W

// pointFree模式
const fp = require('lodash/fp')
const firstLetterToUpper = fp.flowRight(fp.join('. '),fp.map(fp.flowRight(fp.first,fp.toUpper)),fp.split(' '))

console.log(firstLetterToUpper('word wild web'))

函子

函子的概念

函子是函數(shù)式編程里面最重要的數(shù)據(jù)類型,也是基本的運(yùn)算單位和功能單位。

函子首先是一個(gè)容器,它包含了值和值的變形關(guān)系,這個(gè)變形關(guān)系就是函數(shù)。

函子可以把函數(shù)式編程副作用控制在可控的范圍內(nèi),包括處理異常,異步操作等。
一般約定,函子的標(biāo)志就是容器具有map方法。該方法將容器里面的每一個(gè)值,映射到另一個(gè)容器。

函子的基本構(gòu)造

函子就是一個(gè)特殊的容器,它可以由對(duì)象來實(shí)現(xiàn),這個(gè)對(duì)象中包含了值,這個(gè)值永遠(yuǎn)不會(huì)對(duì)外公布,有一個(gè)map方法,用來操作這個(gè)值。還有一個(gè)of方法,用來生成一個(gè)新的容器。

// Functor
class Container {
    static of(value) {
        return new Container(value)
    }
    constructor(value) {
        this._value = value;
    }
    map(fn) {
        return Container.of((fn(this._value)))
    }
}
let r = Container.of(5).map(x => x + 1).map(x => x * x);
console.log('r', r);

這里總結(jié)一下函子的使用

  • 程序運(yùn)算不會(huì)直接操作值,而是通過函子來完成
  • 由map處理后返回的是一個(gè)新的對(duì)象,我們可以繼續(xù)鏈?zhǔn)降牟僮髦?/li>
  • 我們可以把函子想象成一個(gè)盒子,盒子中封裝著一個(gè)值,當(dāng)我恩處理盒子中的值的時(shí)候我們要用到盒子專門改變值的工具:map,我們需要給盒子的map方法傳遞一個(gè)處理值的函數(shù)(純函數(shù)),由這個(gè)函數(shù)來對(duì)值進(jìn)行處理,最終map方法返回一個(gè)包含新值的盒子(函子)。

MayBe函子

函子會(huì)接收各種函數(shù)來處理內(nèi)部的值,這里就有可能遇到錯(cuò)誤,我們需要對(duì)這些錯(cuò)誤做處理,MayBe函子的作用就是對(duì)外部的空值情況做處理。

MayBe函子的構(gòu)造就是在map中設(shè)置空值檢查

class Maybe{
   static of(value){
     return new Maybe(value)
   }

   constructor(val){
     this._value = val
   }
   map(f) {
     return this.val ? Maybe.of(f(this.val)) : Maybe.of(null);
   }
 }

雖然 MayBe函子可以避免出現(xiàn)錯(cuò)誤,但是多次調(diào)用map時(shí)我們并不知道哪里出現(xiàn)了錯(cuò)誤

Either函子

Either函子與if...else處理很相似。它內(nèi)部有兩個(gè)值,左值和右值。右值通常代表正常的值,左值是當(dāng)右值不存在或錯(cuò)誤時(shí)的默認(rèn)值

class Either {
     static of(left,right){
      return new Either (left,right))
    }
    constructor(left,right){
      this.left = left
      this.right = right
    }
    map(fn){
      return this.right ? Either.of(this.left,fn(this.right)) : Either.of(fn(this.left),right)
    }
  }

此外,Either函子另一個(gè)用途是替代try...catch,使用左值來表示錯(cuò)誤

 function parseJSON(json) {
  try {
    return Either.of(null, JSON.parse(json));
  } catch (e: Error) {
    return Either.of(e, null);
  }
}

ap函子

函子中的值有可能是數(shù)值,也有可能是一個(gè)函數(shù),我們想讓值為函數(shù)的函子用另一個(gè)函子中的值運(yùn)算,我們就可以用ap函子

 function add(x) {
    return x + 1
  }
  const A = Functor.of(2)
  const B = Functor.of(add)

  class Ap extends Functor {
    ap(F) {
      return Ap.of(this.val(F.val))
    }
  }
 //我們想讓B函子的值使用A函子的值
  Ap.of(add).ap(Functor.of(2))

凡是部署了ap方法的函子,就是ap函子。ap函子的意義在于對(duì)多參數(shù)的函數(shù),可以從多個(gè)容器中取值,實(shí)現(xiàn)函子的鏈?zhǔn)秸{(diào)用。

Monad 函子

函子中的值可以接受任何值,所以函子之中可以包含另一個(gè)函子。這樣就會(huì)造成函子多層嵌套的問題。取值時(shí)會(huì)很不方便。Monad函子的作用就是:總是返回一個(gè)單層的函子,它有一個(gè)FlatMap方法,與map方法作用相同,唯一的區(qū)別就是如果生成了一個(gè)嵌套函子,它會(huì)取出后者的值,保證返回的永遠(yuǎn)是一個(gè)單層的容器,不會(huì)出現(xiàn)嵌套的情況。

 class Monad extends Functor {
    join() {
      return this.val;
    }
    flatMap(f) {//f是一個(gè)函子
      return this.map(f).join();
    }
  }

如果函數(shù)f返回的是一個(gè)函子,那么this.map(f)就會(huì)生成一個(gè)嵌套的函子。所以,join方法保證了flatMap方法總是返回一個(gè)單層的函子。這意味著嵌套的函子會(huì)被鋪平。

IO函子

I/O是一個(gè)不純的操作,普通的函數(shù)式編程無法處理,所以使用IO函子操作

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));
    }
}
  • IO函子中的_value是一個(gè)一個(gè)函數(shù),這里是把函數(shù)作為值來處理
  • IO函子可以把不純的動(dòng)作存儲(chǔ)到_value中,延遲這個(gè)不純的操作(惰性執(zhí)行),包裝當(dāng)前的操作是純的操作
  • 把不純的操作交給調(diào)用者來處理
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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