用一萬多字從頭到尾介紹【函數式編程】
theme: fancy highlight: atom-one-light
持續創作,加速成長!這是我參與「掘金日新計劃 · 6 月更文挑戰」的第21天,點擊查看活動詳情
Hi~,我是一碗周,如果寫的文章有幸可以得到你的青睞,萬分有幸~
寫在前面
函數式編程是一種很古老的概念,早於第一台計算機的誕生。早於第一台計算機的誕生。
函數式編程是隨着React的流行受到越來越多的關注(就跟Google地圖和Gmail帶火了Ajax一樣)React的高級組件使用了高階函數,高階函數就是函數式編程的一個特性。
-
Vue3也開始擁抱函數式編程了。
-
函數式編程可以拋棄this。
-
打包時可以更好的利用tree shaking 過濾無用代碼。
-
方便測試,方便並行處理。
-
有很多庫可以幫助我們進行函數式開發,例如
lodash
、underscore
、ramda
。
概念
函數是編程(Functional Programming) FP是編程範式之一,我們常説的編程範式還有面向對象編程(面向對象編程就是把現實世界的對象抽象成為程序世界的類和對象,通過封裝繼承多態來演示事物之間的聯繫)和面向過程編程(所謂的面向對象編程就是按照步驟實現),它們是並列關係。
函數式編程的思維方式就是把現實世界中事物和事物聯繫抽象到程序世界(這裏説的抽象是對運算過程進行首抽象)。
程序的本質就是根據輸入的內容,通過某種運算獲得相應的輸出。程序開發過程中會涉及到很多輸入和輸出和函數,假如我們有個x
想要通過某種運算得到結果y
,寫成函數就是y=f(x)
。
函數式編程中的函數指的並不是程序中的函數或者方法,而是數學中的函數,也就是映射關係。例如y=sin(x)
,是x
和y
的映射關係(x
的值確定樂y
的值也就確定了)。
簡單的説函數式編程就是用來描述數據(函數)之間的映射關係。在下面的例子中,就存在面型過程編程以及函數式編程兩種編程範式,示例代碼如下:
```javascript // 面向過程編程 非函數式 var a = 1 var b = 2 var sum = a + b console.log(sum); // 3
// 函數式編程 function add (a, b) { return a + b } var sum = add(1, 2) console.log(sum); // 3 ```
在上面的代碼中,我們需要計算兩個值得和,我們需要先抽象出一個用於計算兩個值功能的函數。
函數式編程的主要特性是代碼的複用。
前置知識
在學習函數式編程之前,我們需要先回顧一下如下知識:
-
頭等函數
-
閉包
頭等函數
頭等函數First-class Function,即可以把函數當做變量一樣使用,主要體現在以下三個方面:
-
將一個函數賦值給一個變量,示例代碼如下:
```javascript // 1. 將函數作為變量使用 function fn () { console.log('this is function'); }
var fun = fn fun() // this is function ```
-
將函數作為參數傳遞給另一個函數,示例代碼如下:
javascript // 定義一個函數,將要作為函數的參數使用 function getHello () { return 'Hello' } // 定義另一個函數,該函數接受一個函數,並將該函數的返回值做處理後並返回 function fun (fn, str) { console.log(`${fn()} ${str}`); } // 執行函數fun() 並將 getHello() 作為參數傳入 fun(getHello, 'JavaScript') // Hello JavaScript
使用好這一特性可以封裝很多高階函數,例如如下函數:
-
forEach
遍歷數組```javascript // forEach const forEach = (array, fun) => { for (let i = 0; i < array.length; i++) { fun(array[i]) } } // 測試 forEach([1, 2, 3], (item) => { console.log(item); })
```
-
filter
返回滿足條件的數組javascript // 2. filter const filter = (array, fun) => { // 定義一個空數組,用於存放符合條件的數組項 let res = [] for (let i = 0; i < array.length; i++) { // 將數組中的每一項都調用傳入的函數,如果返回結果為true,則將結果push進數組,最後返回 if (fun(array[i])) { res.push(array[i]) } } return res } // 測試 let res = filter([1, 2, 3], (item) => { return item > 2 }) console.log(res); // [ 3 ]
-
map
根據回調函數處理我們的數組,並將處理後的結果返回javascript // 3. map const map = (array, fun) => { // 定義一個空數組,用於存放修改後的數據 let res = [] for (let i = 0; i < array.length; i++) { res.push(fun(array[i])) } return res } // 測試 let res = map([1, 2, 3], (item) => { return item * 2 }) console.log(res); // [ 2, 4, 6 ]
-
every
判斷數組中的每一項,如果都滿足回調函數中條件則返回true否則返回falsejavascript // 4. every const every = (array, fun) => { // 假設全員通過 let flag = true for (let i = 0; i < array.length; i++) { // 遍歷數組,如果有一個數組項不合符規則,重新賦值標誌位 if (!fun(array[i])) { flag = false break } } return flag } // 測試 let res = every([1, 2, 3], (item) => { return item < 4 }) console.log(res); // true
-
some
判斷數組中的每一項,如果有一項滿足回調函數中條件就返回true都不滿足則返回falsejavascript // 5. some const some = (array, fun) => { let flag = false for (let i = 0; i < array.length; i++) { if (fun(array[i])) { flag = true break } } return flag } // 測試 let res = some([1, 2, 3], (item) => { return item > 4 }) console.log(res); // false
這麼寫的作用是在編寫過程中屏蔽細節(例如這裏的遍歷),我們只需要關注我們目標就可以(例如遍歷數組中的每一項都進行對2取餘)。
-
-
函數作為返回值使用,示例代碼如下:
```javascript // 3. 作為函數返回值
function sayHello () { return function () { console.log("Hello!"); } } // 調用該函數 // 方式一,定義一個變量接受該返回值,然後將變量以函數的形式調用 const say = sayHello() say() // Hello! // 方法二 通過 ()()的方式調用 sayHello()() // Hello! ```
demo:封裝一個只執行一次的函數
once
。javascript function once (fn) { // 第一次執行的時候,flag 默認為 false let flag = false return function () { // 只有當 flag 為 false 傳遞的函數才執行,執行之後立馬將 flag 重置為true if (!flag) { flag = true // 通過 apply 的方式調用該 fn 函數,並通過 arguments 傳遞參數 return fn.apply(this, arguments) } } }
閉包
作用域鏈
在瞭解閉包之前, 我們先來了解一下作用域鏈。所謂的作用域,就是在運行時代碼中的某些特定部分中變量,函數和對象的可訪問性。
在ECMAScript5版本中不存在塊級作用域,也就是説在代碼塊中的定義的變量是存在全局作用域的,示例代碼如下:
javascript
if (true) {
var a = 100;
}
console.log(a); // 100
在ECMAScript5版本中也沒有跨級作用域,但它有函數作用域,也就是説,在某函數內定義的所有變量在該函數外是不可見的。示例代碼如下:
javascript
function fun() {
var b = 200;
}
console.log(b); // ReferenceError: b is not defined
具體什麼是作用域鏈,看一下下面的代碼:
```javascript // 作用域鏈 var a = 'a'; // 全局變量
function fun() { var b = 'b'; // 相對於fn函數作用域的話,b相當於全局變量
function fn() { var c = 'c'; // 相對於f函數作用域的話,c相當於全局變量 function f() { // 函數作用域 var d = 'd'; console.log(a); console.log(b); console.log(c); console.log(d); } f(); } fn(); } fun(); ```
在f()
函數中輸出變量a
,父級沒有,再往父級找,如果還沒有就一直往父級尋找,直到找到全局作用域。這種一層一層關係就是作用域鏈。
閉包
在函數中提出的概念,簡單來説就是一個函數定義中引用了函數外定義的變量,並且該函數可以在其定義環境外被執行。當內部函數以某一種方式被任何一個外部函數作用域訪問時,一個閉包就產生了。
實際上閉包可以看做一種更加廣義的函數概念。因為其已經不再是傳統意義上定義的函數。
-
閉包的條件:
-
外部函數中定義了內部函數。
-
外部函數是具有返回值,且返回值為內部函數。
-
內部函數還引用了外部函數的變量。
-
-
閉包的缺點:
-
作用域沒有那麼直觀。
-
因為變量不會被垃圾回收所以有一定的內存佔用問題。
-
-
閉包的作用:
-
可以使用同級的作用域。
-
讀取其他元素的內部變量。
-
延長作用域。
-
-
閉包的實現的demo:
javascript // 1. 通過返回的內部函數來操作函數中的局部變量 function fun () { var v = 100; // 局部變量 // 通過返回一個對象的方式訪問局部變量v 來完成閉包 return { set: function (x) { v = x; }, get: function () { return v } } } var result = fun(); result.set(200) console.log(result.get()); // 200
``javascript // 2. 定義一個局部變量,計算該函數一共調用幾次 var generate_count = function () { var container = 0; return function () { container++ console.log(
這是第${container}次調用`); } }var result = generate_count(); result(); // 這是第1次調用 result(); // 這是第2次調用 result(); // 這是第3次調用 ```
```javascript // 3.修改 Math.pow() 函數,讓求一個數的平方或者立方時,不需要每次傳遞第二個參數
/ Math.pow(4, 2) // 求4的平方 Math.pow(4, 3) // 求4的立方 /
// 寫一個函數生成器 function makePower (power) { return (number) => { return Math.pow(number, power) } } // 平方 let power2 = makePower(2) // 立方 let power3 = makePower(3) // 求4的平方 console.log(power2(4)) // 16 // 求4的立方 console.log(power3(4)) // 62 ```
純函數
概念
純函數指的是相同的輸入,永遠會得到相同的輸出,而且沒有任何可觀察的副作用。純函數類似數學中的函數,用來描述輸入和輸出的關係,y=f(x)
。如下圖所示:
在上圖中,函數
f
描述了左邊值到右邊值的對應關係。
下面我們看一下JavaScript提供的兩個函數slice
和splice
,這兩個函數一個是純函數,一個不是純函數。示例代碼如下:
-
slice
返回數組的指定部分,不會改變原有數組,這是一個純函數。```javascript // Array.slice() 函數,返回數組的指定部分,並不會改變原數組
let array = [1, 2, 3, 4, 5]
// 多次調用 slice() 方法,傳入相同的參數 console.log(array.slice(0, 3)) // [1, 2, 3] console.log(array.slice(0, 3)) // [1, 2, 3] console.log(array.slice(0, 3)) // [1, 2, 3] // 多次調用返回的結果相同,該函數為純函數 ```
-
splice
對數組進行操作,並返回操作後的數組,該函數會改變原有數組,並不是一個純函數。```javascript let array = [1, 2, 3, 4, 5] // Array.splice() 函數,該函數對指定數組進行操作,並返回操作後得數組,該函數會改變原有數組
console.log(array.splice(0, 3)) // [1, 2, 3] console.log(array.splice(0, 3)) // [ 4, 5 ] console.log(array.splice(0, 3)) // [] // 多次調用返回的結果並不相同,所以該函數並不是一個純函數
```
如果自己寫一個純函數也非常簡單,只要有輸入(也就是參數)和輸出(也就是返回值),並且相同的輸入可以得到相同的輸入,説明該函數就是一個純函數。實現純函數的代碼如下:
javascript
function add (a, b) {
return a + b
}
函數式編程是不會保留計算的中間結果的,所以説變量是不可變的,也就是無狀態的。
由於純函數的特性,我們可以把一個函數的結果交給另一個函數來處理,也就是函數組合。
Lodash庫為我們提供了一些函數柯里化和函數組合的一些工具函數。
純函數的好處
-
可緩存:
純函數是可以根據輸入來做緩存的,如果每次輸入的結果都是一樣的,就可以將第一次的結果緩存起來,在下次調用的時候直接返回結果即可。
實現緩存技術的一種經典的方式就是
memoize
技術,Lodash中實現了該方法,示例代碼如下:```javascript const _ = require('lodash') // 定義一個計算圓面積的函數 function getArea (r) { console.log(r) return Math.PI * r * r }
// 調用 Lodash 中提供的memoize方法生成一個可緩存的函數 let getAreaWithMemory = _.memoize(getArea)
// 多次調用 getAreaWithMemory 函數 console.log(getAreaWithMemory(10)) console.log(getAreaWithMemory(10)) console.log(getAreaWithMemory(10)) console.log(getAreaWithMemory(10))
/ 結果如下 10 314.1592653589793 314.1592653589793 314.1592653589793 314.1592653589793 / ```
由結果可以看出,我們調用了4次函數,但是最終只打印了一次
r
,所以説最終只執行了一次getArea
函數,剩下的每次都是從緩存中拿的結果。我們還可以自己簡單手寫一個
memoize
函數,雖然不具備健壯性。實現代碼如下:javascript function memoize (fn) { // 定義一個對象,用於存儲執行結果 let cache = {} return function () { // 將當前的參數用作 key 保證同一參數可以不用重複執行函數 let key = JSON.stringify(arguments) // 如果在當前對象中具有 key 的值,就直接返回該值,否則調用傳入的fn方法,並將結果存入這個對象中 cache[key] = cache[key] || fn.apply(fn, arguments) return cache[key] } }
執行結果與上面相同。
-
可移植性/自文檔化:
純函數是完全自給自足的,它需要的所有東西都能輕易獲得。這樣的好處是函數的依賴很明確,因此更易於觀察和理解。
-
可測試:
純函數讓測試變得更加的方便,單元測試其實在斷言輸出結果,所有的純函數都是有輸入有輸出,所有説有利於測試。
-
並處處理:
在多線程下,多個線程去同時修改一個變量可能會出現意外的情況。而純函數是一個封閉的空間,它只依賴於參數,不會訪問共享的內存數據,所以在並行環境下,可以隨意的執行純函數。
並處可以通過Web Worker技術實現,但是現在主流的還是隻用單線程。
純函數的副作用
我們先來看一下如下代碼:
```javascript // 不純的函數 let mini = 18 function checkAge (age) { return age >= mini } // 由於函數中的判斷條件的變量定義在全局,只要全局變量mini發生了改變,就導致我們每次的輸入可能得不到相同的輸入,這就是一個不純的函數
// 純函數
function checkAge (age) { let mini = 18 return age >= mini } // 上面的函數是一個純函數,但是存在硬編碼,導致這個函數不靈活,可以通過函數柯里化來解決 ```
如果函數依賴於外部的狀態就無法保證輸出相同。就會帶來副作用,副作用的來源可能是:
-
配置文件
-
數據庫
-
用户的輸入
-
發送一個http請求
-
可變數據
-
打印/log
-
獲取用户輸入
-
DOM查詢
-
訪問系統狀態
-
...
所有的外部交互都可能帶來副作用,副作用讓方法的通用性下降,不適合擴展和可重用。但是副作用是不能完全禁止的,所以我們要儘可能的將副作用控制在可控範圍內發生。
柯里化
柯里化可以把接收多個參數的函數轉換可以具有任意參數的函數,並且返回接收剩餘參數且返回結果的新函數。柯里化可以給函數組合提供細粒度的函數。
入門
下面一段代碼來演示一下函數的柯里化:
javascript
// 下面這個函數 存在硬編碼
/*
function checkAge (age) {
let mini = 18
return age >= mini
}
*/
// 將其修改為不具有硬編碼的純函數
function checkAge (min, age) {
return age >= min
}
我們優化後的函數不具有硬編碼,且不受外部變量影響的一個純函數。
如果我們經常使用某個基準值,則可以將代碼進行復用。
javascript
console.log(checkAge(18, 20))
console.log(checkAge(18, 22))
console.log(checkAge(18, 24))
我們將函數優化為如下:
```javascript function checkAge (min) { // 通過閉包,將 min 進行緩存 return function (age) { return age >= min } } let checkAge18 = checkAge(18)
console.log(checkAge18(20)) console.log(checkAge18(22)) console.log(checkAge18(24)) ```
用ES6改寫函數:
javascript
const checkAge = min => age => age >= min
// 完整寫法如下:
const checkAge = (min) => {
return (age) => {
return age >= min
}
}
因為在ES6中,如果只有一個參數()
可以省略,如果只有return
的一句話,{}
和return
都可以省略。
其實上面的代碼就是函數的柯里化:
-
當一個函數有多個參數的時候可以先傳遞一部分參數調用它(這部分參數以後永遠不變)
-
然後返回一個新的函數接受剩餘的參數,返回結果。
但是上面的例子柯里化並不徹底,我們需要將任何函數轉換為柯里化函數。
Lodash裏的柯里化
Lodash提供了一個curry()
函數,該函數的語法結構如下:
javascript
_.curry(fun)
-
功能:該函數創造一個柯里化函數,接受一個或者多個
fun
參數,如果fun
所需要的參數都被提供則執行fun
,並返回執行結果,否則繼續返回該函數並等待接受剩餘的參數。 -
參數:需要被柯里化的函數。
-
返回值:柯里化後的函數。
示例代碼如下:
```javascript const _ = require('lodash') // 定義一個多元函數,所謂的多元函數,就是具有多個參數的函數,一個參數叫一元函數 function getSum (a, b, c) { return a + b + c }
// 通過 .curry() 函數將其轉換為一個柯里化函數 let curried = .curry(getSum) // 説明getSum1還需要接收兩個參數才可以執行 const getSum1 = curried(1) console.log(getSum1(2, 3)) // 6
// 説明getSum2 還需要接收一參數才可以執行
// const getSum2 = curried(1, 2) // 或者 // const getSum2 = getSum1(2) // 或者 const getSum2 = curried(1)(2)
console.log(getSum2(3)) // 6
```
demo
判斷字符串中的空白和數字,可以使用match
方法實現。示例代碼如下:
javascript
// 判斷字符串中空格和數字
console.log('Hello JavaScript'.match(/\s+/g))
console.log('Hello 123 JavaScript'.match(/\d+/g))
我們可以將上面的代碼通過柯里化的方式重新定義,最終可以根據不同的正則表達式,生成對應的匹配函數。
```javascript const _ = require('lodash')
// 調用_.curry() 傳遞一個匿名箭頭函數生成一個柯里化函數 const match = _.curry((reg, str) => { return str.match(reg) }) // 生成一個匹配空格的函數 const haveSpace = match(/\s+/g) console.log(haveSpace('Hello JavaScript')) // 同理 生成一個匹配數字的函數 const haveNumber = match(/\d+/g) console.log(haveNumber('Hello 123 JavaScript')) ```
如果我們想要判斷數組中的每一項元素是否存在空格或者數字,還可以繼續改造。
```javascript const _ = require('lodash') const match = _.curry((reg, str) => str.match(reg)) const haveSpace = match(/\s+/g) const haveNumber = match(/\d+/g) // 如果我們想要判斷數組中的每一項元素是否存在空格或者數字,還可以繼續改造
// 生成一個新的函數filter,它是一個柯里化函數 const filter = _.curry((fun, array) => { return array.filter(fun) })
// 通過 filter 生成一個函數 findSpace, 該函數可以接受一個數組,返回擁有空白字符串的元素 const findSpace = filter(haveSpace)
console.log(findSpace(['一碗粥', '一碗周'])); ```
這些函數只需要定義一次,以後就可以無數次的進行使用。
_.curry()函數模擬
調用_.curry()
函數返回的函數有兩種調用方法方式,第一種是當傳遞的參數個數定等於原來函數參數個數時,立刻執行該函數;另一種就是當傳遞的參數個數小於原來參數個數時,返回一個新的函數並等待剩餘的參數傳遞。
我們想要模擬這個函數,只需要實現這兩種形式的調用即可。實現代碼如下:
```javascript // 該函數接受一個函數作為參數 function curry (func) { // ...args 接收所有參數 return function curried (...args) { // 判斷實參個數與形參個數,args.length表示實參個數,func.length可以獲取形參個數 if (args.length < func.length) { // 如果實參個數小於形參個數,則説明需要繼續等待參數傳遞,則繼續返回一個函數 return function () { // 通過閉包可以獲取第一次傳入的參數 args // 第二次傳遞的參數可以通過 arguments 獲取,通過Array.from() 將其轉換為一個數組,並通過 Array.concat() 方法將其將兩個數組合並 // 最後通過 ... 運算符展開數組,作用參數遞歸調用函數 return curried(...args.concat(Array.from(arguments))) } } // 如果不小於則直接調用該函數,並將結果返回 return func(...args) } }
function getSum (a, b, c) { return a + b + c }
let curried = curry(getSum) console.log(curried(1)(2)(3)); ```
現在我們就實現了這個_.curry()
函數
總結
柯里化可以讓我們給一個函數傳遞較少的參數,得到一個已經記住了某些固定參數的新函數。
這是一種對參數緩存的辦法。
這樣做可以讓函數變得更加靈活,讓函數的顆粒度更小,可以將多元函數轉換為一元函數,可以組合使用函數產生強大的功能。
函數組合
概念
我們用純函數和柯里化很容易寫出洋葱代碼,所謂的洋葱代碼就是指的是一個函數的參數是另一個函數結果,另一個函數的參數又是另一個的另一個函數的結果,簡單的偽代碼就是h(g(f(x)))
。
我們來寫一個洋葱代碼,例如獲取數組的最後一個元素在轉換為大寫字母,示例代碼如下:
javascript
_.toUpper(_.first(_.reverse(array)))
這一層包裹着另一層的代碼,難以閲讀和理解,我們把他們稱為洋葱代碼。
函數組合可以幫助我們解決這個問題,它可以將細粒度的函數重新組合生成一個新的函數。
管道
在學習函數組合之前,我們先來學習一下管道的概念。
如果我們給fn
函數傳遞參數a
,返回結果b
,我們可以把整個數據的處理過程看做一個黑盒管道,如下圖所示:
參數a
經過了管道的處理最終得到了b
,但是如果我們中間出現了問題,導致最終的結果並不是結果b
,我們定位問題的話並不是很方便,因為這個fn
管道太長了,並不知道具體的問題出現了了哪裏。
這個時候我們可以將fn
函數拆分為多個小函數,在運算的過程中就多了需要中間值,然而我們並不需要考慮這些中間結果,只需要關注輸入的值和最終輸出的值即可。如下圖所示:
在上面圖中,我們就將一個大的管道(函數)拆分成為了多個小的函數,執行每個函數都會得到一個結果,這些結果並不是我們最終想要的一個結果,而是每次的中間值,這些中間值具體是什麼不用做考慮。
最後我們將每次執行的結果用作下一次函數執行的參數,然後組合成一個大的函數,偽代碼如下:
javascript
fn = compose(fn1, fn2, fn3)
b = fn(a)
函數組合
函數組合(compose)指的是如果一個函數要經歷多個函數處理才可以得到最終的值,這個時候可以把中間的過程的函數合併稱為一個函數。
函數組合有着如下的特點:
-
函數就像是數據的管道,函數組合就是把這些管道連接起來,讓數據通過多個管道形成最終的結果。
-
函數組合默認是從右到左執行(更加能夠反映數學上的含義)。
現在我們就利用函數組合的特性,來編寫一個demo:編寫一個函數求出數組的最後一個元素,示例代碼如下:
```javascript // 定義函數組合函數的函數 function compose (f, g) { // 接受多個函數,返回一個函數,這個函數接收一個輸入。 return function (value) { // 這麼寫的化洋葱代碼並沒有減少,而是封裝了起來 return f(g(value)) } }
// demo // 反轉數組的函數 function reverse (array) { return array.reverse() } // 返回數組第一項的函數 function first (array) { return array[0] }
// 使用組合函數 const last = compose(first, reverse) console.log(last([1, 2, 3, 4, 5, 6])) // 6 ```
雖然有很多種求數組中最後一個元素的方法,但是我們使用函數組合的方法,可以對函數進行任意的組合,且我們封裝的函數可以複用。
Lodash中的函數組合
Loadsh中提供了兩個函數,一個是flow
另一個是flowRight
,該函數的功能是創建一個函數。 參數是任意個函數,返回的結果是調用提供函數的結果,this
會綁定到創建函數。 每一個連續調用,傳入的參數都是前一個函數返回的結果。
兩個函數區別就是一個是從左到右運行的(flow
),另一個是從右到左運行的(更加能夠反映數學上的含義),所以説flowRight
用的更多一些。
示例代碼如下如下:
```javascript const _ = require('lodash')
// 需求:將數組最後一個元素取出,並轉換為大寫
const reverse = arr => arr.reverse()
const first = arr => arr[0]
const toUpper = str => str.toUpperCase()
const f = _.flowRight(toUpper, first, reverse)
console.log(f(['bi', 'an', 'fan', 'hua'])) // HUA ```
模擬Lodash中的函數組合
接下來我們就模擬一下Lodash中的flowRight
函數。
需求分析:該函數的可以接受多個參數,且均為純函數的形式。執行後返回一個函數,該函數就接收參數,並且從右到左的傳遞處理該數據並且進行處理。
實現該功能主要是通過Array.prototype.reduce()
實現,該方法的語法請參考
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce
實現代碼如下:
```javascript // 通過 ...args 來接受任意和函數參數 function compose (...args) { // 返回一個函數,用於接受第一個傳遞的值 return function (val) { // 因為是從右往左調用,需要將數組進行翻轉之後再調用 reduce 方法。或者直接調用 reduceRight()
/*
reduce 作用是對數組中的每個值調用指定的callback,並將結果彙總後返回 接收兩個參數,callback, value
* callback 中包含4個參數,這裏只用到了兩個,第一個是累計的值,第二個是當前值
* value 表示第一次調用時的值
*/
return args.reverse().reduce(function (acc, fn) {
// 返回一個函數調用的結果,將次結果做累計,為下次調用做基礎
return fn(acc)
}, val)
}
}
// 用箭頭函數改寫 // const compose = (...args) => val => args.reduceRight((acc, fn) => fn(acc), val) ```
結合律
函數的組合需要滿足結合律(associativity),我們看一下下面的代碼:
javascript
let f = compose(f, g, h)
let associative = compose(compose(f, g), h) === compose(f, compose(g, h))
上面的代碼就是結合律特性。
示例:
```javascript const _ = require('lodash')
const f1 = .flowRight(.toUpper, .first, .reverse)
const f2 = .flowRight(.flowRight(.toUpper, .first), _.reverse)
const f3 = .flowRight(.toUpper, .flowRight(.first, _.reverse))
console.log(f1(['bi', 'an', 'fan', 'hua'])) // HUA console.log(f2(['bi', 'an', 'fan', 'hua'])) // HUA console.log(f3(['bi', 'an', 'fan', 'hua'])) // HUA ```
如何調試組合函數
我們的組合函數,每次只有一句話,但是出現問題的時候,並不知道是哪個函數出現了問題,調試的話是非常的不容易的,那組合函數應該怎調試呢?先看一段代碼:
```javascript const _ = require('lodash')
/ 需求 將字符串 BI AN FAN HUA 轉換為 bi_an_fan_hua /
/ Lodash 中的 split 函數接受兩個參數,第一個是 需要被分割的字符串,第二個是分隔符 但是如果是這樣的話,並不適合寫組合函數,因為組合函數只能接受一個參數 所以我們需要對該函數進行一下改造,將其改造為柯里化 / const split = .curry((separator, str) => .split(str, separator)) // 以空格拆分字符串 const splitSpace = split(' ')
// 大小轉換小寫採用 _.toLower 函數
/ 將一個數組拼接為一個字符串可以使用 _.join 函數,該函數接受兩個參數,第一個是數組,第二個是拼接符號 這個也不適合寫組合函數,所以也需要改造並柯里化 / const join = .curry((separator, array) => .join(array, separator)) // 以_拼接 const join_ = join('_')
const str = 'BI AN FAN HUA'
// 組合函數 const f = .flowRight(join, _.toLower, splitSpace) // 測試結果 console.log(f(str)); ```
最終的執行結果如下:
純文本
b_i_,_a_n_,_f_a_n_,_h_u_a
這個結果並不是我們想要的那個結果,那問題出現在哪裏了呢?
想要調試組合函數,我們可以編寫一個純函數用於調試,該純函數接受一個值,並不做任何處理直接將該值返回,但是在返回之前需要做一些事情,例如打印上次執行的結果。該測試函數的定義如下:
javascript
// 定義一個測試函數,因為該函數需要兩個參數,需要將其柯里化
const log = _.curry((name, value) => {
console.log(`${name}的結果為: `, value)
return value
})
我們的函數組合定義修改如下:
javascript
const f = _.flowRight(join_, log('toLower'), _.toLower, log('splitSpace'), splitSpace)
最終的執行結果為:
純文本
splitSpace的結果為: [ 'BI', 'AN', 'FAN', 'HUA' ]
toLower的結果為: bi,an,fan,hua
b_i_,_a_n_,_f_a_n_,_h_u_a
我們根據結果得知,join
的參數應該是一個數組,但是傳遞的卻是一個字符串,所以需要對toLower
函數做一下改造,改造代碼如下:
javascript
// 大小轉換小寫採用 _.toLower 函數
/*
_.map函數對數組內的值通過回調函數進行修改,但是map並不是只接受一個參數,所以也需要將map進行柯里化
*/
const map = _.curry((callback, array) => _.map(array, callback))
// 使用 _.toLower 作為 callback
const mapToLower = map(_.toLower)
我們的函數組合定義修改如下:
javascript
const f = _.flowRight(join_, log('toLower'), mapToLower, log('splitSpace'), splitSpace)
到此為止,就實現了我們的需求。
完整代碼如下:
javascript
const _ = require('lodash')
const log = _.curry((name, value) => {
console.log(`${name}的結果為: `, value)
return value
})
const split = _.curry((separator, str) => _.split(str, separator))
const splitSpace = split(' ')
const map = _.curry((callback, array) => _.map(array, callback))
const mapToLower = map(_.toLower)
const join = _.curry((separator, array) => _.join(array, separator))
const join_ = join('_')
const str = 'BI AN FAN HUA'
const f = _.flowRight(join_, log('toLower'), mapToLower, log('splitSpace'), splitSpace)
console.log(f(str));
Lodash 中的FP模塊
在實現上面的需求是,需要反覆對Loadsh中的函數進行柯里化處理,這顯然是比較麻煩的。
Lodash中包含一個FP模塊,該模塊中提供了一些比較實用的對函數式編程友好的函數,提供了不可變的函數,這些函數都是已經柯里化的,函數優先,數據滯後的。
示例代碼如下:
```javascript const _ = require('lodash') const fp = require('lodash/fp')
// 對比一下 _.split() 與 fp.split() 的區別
const r1 = _.split('一 碗 周', ' ') // 數據優先 函數滯後,未柯里化 console.log(r1) // [ '一', '碗', '周' ]
const r2 = fp.split(' ')('一 碗 周') // 自動柯里化,函數優先,數據滯後 console.log(r2) // [ '一', '碗', '周' ]
```
根據這個特性我們改寫一下上面那個案例:
javascript
const fp = require('lodash/fp')
const str = 'BI AN FAN HUA'
const f = fp.flowRight(fp.join('_'), fp.map(fp.toLower), fp.split(' '))
console.log(f(str)) // bi_an_fan_hua
可以看到,代碼量巨減。(牛啊牛啊)
值得注意的是,
lodash
和lodash/fp
中的map
函數有所不同。
Pointfree編程風格
Pointfree是一種編程風格,這種風格要求我們把數據處理的過程定義成與數據無關的合成運算,不需要關注代表數據的參數,只需要將簡單的運算步驟聚合到一起,在使用這種模式之前,我們需要定義一些輔助的基本運算函數(函數組合)。
我們可以將上面概括為以下三點:
-
不需要指明處理的數據。
-
只需要合成運算過程。
-
需要定義一些輔助的基本運算函數。
示例代碼如下:
```javascript / 需求:將 Hello World 轉換為 hello_world /
// 非 Point Free 編程風格 function f (word) { // 先將字母轉換為全部小寫,在匹配所有的空格替換為_ return word.toLowerCase().replace(/\s+/g, '_') }
// Point Free 風格代碼 const fp = require('lodash/fp') // 過程中,不關心處理的數據 const f = fp.flowRight(fp.replace(/\s+/g, '_'), fp.toLower) ```
函數組合其實就是Pointfree編程風格的代碼。
demo:把一個字符串中的首字母提取,並轉換成為大寫,使用.
作為分隔符。示例代碼如下:
```javascript / 需求:將 world wild web 轉換為 W. W. W / const fp = require('lodash/fp')
/ 分析需求 1. 將字符串以空格進行拆分 2. 提取拆分後的第一個字母,並將其轉換為大寫 3. 以. 進行拼接 / // * 函數式編程時,函數名儘量要具有語義 const firstLetterToUpper = fp.flowRight( fp.join('. '), / 在函數組合中嵌套一個函數組合 / fp.map(fp.flowRight(fp.first, fp.toUpper)), fp.split(' ') ) console.log(firstLetterToUpper('world wild web')) // W. W. W
```
函子
什麼是函子
在函數式編程中,函子(functor)是受到範疇論函子啟發的一種設計模式,它允許泛化類型在內部應用一個函數而不改變泛化類型的結構。
學習好函子這個概念,對以後的函數式編程很重要,那到底什麼是函子的?舉一個例子來解釋一下什麼是函子。
假如我要給我住在美國的二姑快遞一些保定的驢肉,很明顯我不可以買好驢肉直接送過去,需要將驢肉包裝,那麼這個驢肉就是被容器化的肉,然後將這個包裝好的驢肉交給快遞員,我會告訴快遞員這個驢肉的打開方法,因為通過海關時候需要打開進行檢疫、蓋上郵戳、重新包裝,最後在送往美國。
以上例子並不是肉自己走進包裝盒裏面的,而是容器化之後的肉,有包裝的方法,比如陰冷保存、速食、向上打開等注意事項、防止海關檢查的時候不小心損壞。
我們用代碼實現如下:
javascript
// 存放肉的盒子
class MeatBox {
constructor(meat) {
this._value = meat
}
// map 表示打開包裝的方法
map (fn) {
return new MeatBox((fn(this._value)))
}
}
海關打開之後檢疫完成,他又會根據盒子的規範重新打包成新的肉容器,方便在美國海關檢疫,然後送到二姑手上。
實現流程代碼如下:
```javascript // 將肉容器化 let meatBox1 = new MeatBox('驢肉')
// 海關檢疫,然後將驢肉進行包裝快遞。 let meatBox2 = meatBox1.map(meat => check(meat))
// 二姑吃到的保定的驢肉,然後將驢肉包裝起來進行存放 let meatBox3 = meatBox2.map(meat => eat(meat)) ```
用鏈式寫法
javascript
new MeatBox('驢肉').map(meat => check(meat)).map(meat => eat(meat))
我們總結一下上面的幾個特點:
-
肉從一個單體或者一個值被容器化了,變成了一個具有數據類型的容器。
-
每一次對肉操作都會將肉拿出來進行操作之後又重新根據規則或者某種協議進行容器化。
-
容器具有
map
這個方法,用於取值,並且返回的也具有map方法。 -
可以進行鏈式調用。
所謂的函子就是值被容器化之後具有一條標準協議規範的數據類型或者數據容器。
函子的概念如下:
-
函數遵守一些特定規則的類型容器獲取數據編程協議。
-
具有一個通用
map
方法,該方法返回新實例,這個實例和之前實例有相同的規則。 -
具有與結婚外部運算能力。
為什麼使用函子
在講解函子之前,我們需要了解為什麼要有函子。
先看下面的代碼:
```javascript function double (x) { return x * 2 } function add5 (x) { return x + 5 }
var a = add5(5) double(a) // 或者 double(add5(5)) ```
我們想要以數據為中心,串行的方式去執行
javascript
(5).add5().double()
很明顯,這樣的串行調用就清晰多了。但是要實現這樣的串行調用,需要(5)
必須是一個引用類型,因為需要掛載方法。同時,引用類型上要有可以調用的方法也必須返回一個引用類型,保證後面的串行調用。
javascript
class Num {
constructor (value) {
this.value = value ;
}
add5 () {
return new Num( this.value + 5)
}
double () {
return new Num( this.value * 2)
}
}
var num = new Num(5);
num.add5 ().double ()
我們通過new Num(5)
,創建了一個num
類型的實例。把處理的值作為參數傳了進去,從而改變了 this.value
的值。我們把這個對象返會出去,可以繼續調用方法去處理數據。
通過上面的做法,我們已經實現了串行調用。但是,這樣的調用很不靈活。如果我想再實現個減一的函數,還要再寫到這個 Num 構造函數裏。所以,我們需要思考如何把對數據處理這一層抽象出來,暴露到外面,讓我們可以靈活傳入任意函數。來看下面的做法:
javascript
class Num {
constructor (value) {
this.value = value ;
}
map (fn) {
return new Num( fn(this.value) )
}
}
var num = new Num(5);
num.map(add5).map(double)
我們創建了一個map
方法,把處理數據的函數fn
傳了進去。這樣我們就完美的實現了抽象,保證的靈活性。
理解函子
現在編寫一個簡單的函子,實例代碼如下:
```javascript class Container { constructor(value) { // 函子中的值是保存在內部的,不對外部公佈 // 定義私有成員使用_開頭作用約束 this._value = value } // 有一個對外的方法map,接受一個函數(純函數),來處理私有的值 map (fn) { // 返回一個新的函子,把fn處理的值傳遞給函子,有新的函子來保存 return new Container(fn(this._value)) } } // 創建一個函子對象 let r = new Container(5) .map(x => x + 1) // 6 .map(x => x * x) // 36
// 返回了一個Container對象,其具有一個_Value的值,不對外部公佈 console.log(r) // Container { _value: 36 } ```
Pointed函子
Pointed函子是實現了of
靜態方法的函子。因為上面的代碼中使用的是面向對象的編程方式,在函數式編程中,應該避免使用new
關鍵字,於是Pointed函子就出現了。
of
靜態方法是為了避免使用new
關鍵字來關鍵對象,創建對象的實現在of
靜態方法中實現。
這裏更深層的含義就是of
方法用來將值放到上下文中。實現代碼如下:
```javascript class Container { static of (value) { //使用類的靜態方法,of替代了new Container的作用 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) // 6 .map(x => x * x) // 36
console.log(r) // Container { _value: 36 } ```
但是上面的代碼有一個問題,如果我們傳遞null
或者undefined
就會出現意想不到的結果,導致我們的函數變得不純,代碼如下:
```javascript class Container { static of (value) { //使用類的靜態方法,of替代了new Container的作用 return new Container(value) } constructor(value) { this._value = value } map (fn) { return Container.of(fn(this._value)) } } let r = Container.of(null) .map(x => x + 1) .map(x => x * x)
console.log(r) ```
想要解決這個問題,我們接着往下看
MayBe函子
MayBe函子可以解決上面出現的問題,並作出相應的處理,其作用就是可以對外部空值得情況做處理(控制副作用在允許的範圍)實現代碼如下:
```javascript class MayBe { static of (value) { return new MayBe(value) } constructor(value) { this._value = value } map (fn) { // 如果當期值為 null 或者 undefined 將 null 傳遞作為值傳遞給當前類 return this.isNothing() ? MayBe.of(null) : MayBe.of(fn(this._value)) } // 定義一個輔助方法,用於判斷當前是否存在問題 isNothing () { return this._value === null || this._value === undefined } }
// 正常傳遞 const r1 = MayBe.of('hello world').map(val => val.toUpperCase()) console.log(r1) // MayBe { _value: 'HELLO WORLD' }
// 傳遞一個null const r2 = MayBe.of(null).map(val => val.toUpperCase()) console.log(r2) // MayBe { _value: null }
```
MayBe函子也存在一個問題,就是鏈式調用次數過多時,我們並不知道那個環節出現了問題。如下代碼:
```javascript const r3 = MayBe.of('hello world') .map(val => val.toUpperCase()) .map(x => undefined) .map(val => val.toUpperCase())
console.log(r3) // MayBe { _value: null }
```
在上面的代碼中,我們並不知道那個環節出了問題。
Either函子
條件運算if...else
是最常見的運算之一,函數式編程裏面,使用 Either 函子表達。
Either 函子內部有兩個值或者是具有兩個類:左值(Left
)和右值(Right
)。右值是正常情況下使用的值,左值是右值不存在時(出現異常時)使用的默認值。示例代碼如下:
```javascript // 一個類定義兩個值的寫法 class Either { static of (left, right) { return new Either(left, right) } constructor(left, right) { this._left = left this._right = right } map (fn) { // 如果 right 的值存在,left 的值原封不動的返回,否則反之 return this._right ? Either.of(this._left, fn(this._right)) : Either.of(fn(this._left), this._right) } }
// * 定義兩個類的寫法的寫法
// 出現異常時調用 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 (value) { return Right.of(fn(this._value)) } }
// 有一個可能會出現異常的需求,將字符串轉換為JSON對象 // 這裏通過 try 來捕獲異常,如果出現異常,則將值保存為left,否則正常處理
// * 一個類的寫法 function parseJSON1 () { try { return Either.of(null, JSON.parse('{name: "一碗粥"}')) } catch (e) { console.log(e.message); return Either.of('{name: “一碗周”}', null) } } // * 兩個類的寫法 function parseJSON2 () { try { return Right.of(JSON.parse('{name: 一碗粥}')) } catch (e) { console.log('報錯了'); return Left.of('{name: 一碗粥}') } } console.log(parseJSON1()) // Either { _left: null, _right: { name: '一碗粥' } } console.log(parseJSON2()) // Unexpected token n in JSON at position 1 Right { _value: '[name: ‘一碗粥’]' }
```
IO函子
IO就是Input and Output,即輸入輸出,IO函子中的_value
是一個函數,這裏是把函數作為值來處理。
IO函子可以把不純的函數存儲到_value
中,延遲執行這個不純的操作(惰性執行),包裝當前的操作為純函數。
把不純的操作交給調用者來使用(把不純的函數延遲執行到調用時),示例代碼如下:
```javascript // 這裏需要將函數組合 const fp = require('lodash/fp')
class IO { static of (value) { // 將傳遞的值 value 通過函數包裹起來了,把求值延遲了。需要調用_value // IO 函子最終還是想要一個結果 需要值的時候再取值 return new IO(() => { return value }) } constructor(fn) { this._value = fn } map (fn) { // map 返回一個函數組合,第一個函數為調用of傳遞的函數,所以調用map時只需要傳遞一個fn函數即可 return new IO(fp.flowRight(fn, this._value)) } } // 獲取當前進程執行的路徑 let r = IO.of(process).map(process => process.execPath) console.log(r._value()) // C:\Program Files\nodejs\node.exe ```
IO函子內部幫我們包裝了一些函數,當然我們傳遞的函數有可能是不純的操作,我們不管這個操作是不是純的,IO函子返回的結果始終是純的操作,我們調用map的時候,始終會返回一個IO函子。
而_value
屬性保留的組合函數,有可能是不純的,我們在執行時調用它,控制了副作用在可控的範圍內發生。
Task函子
-
函子可以控制副作用,還可以處理異步任務,為了避免地獄之門。
-
異步任務的實現過於複雜,我們使用
folktale
中的Task
來演示。 -
folktale一個標準的函數式編程庫。和
lodash
、ramda
不同的是,他沒有提供很多功能函數。只提供了一些函數式處理的操作,例如:compose
、curry
等,一些函子Task
、Either
、MayBe
等。
安裝:
powershell
npm i folktale --save
folktale中的curry函數
```javascript const { compose, curry } = require('folktale/core/lambda')
// curry中的第一個參數是函數有幾個參數,為了避免一些錯誤 const f = curry(2, (x, y) => x + y)
console.log(f(1, 2)) // 3 console.log(f(1)(2)) // 3 ```
folktale中的compose函數
```javascript const { compose, curry } = require('folktale/core/lambda') const { toUpper, first } = require('lodash/fp')
// compose 組合函數在lodash裏面是flowRight const r = compose(toUpper, first) console.log(r(['one', 'two'])) // ONE ```
Task函子異步執行
```javascript const { task } = require('folktale/concurrency/task') const fs = require('fs') // 2.0中是一個函數,函數返回一個函子對象 // 1.0中是一個類
//讀取文件 function readFile (filename) { // task傳遞一個函數,參數是resolver // resolver裏面有兩個參數,一個是reject失敗的時候執行的,一個是resolve成功的時候執行的 return task(resolver => { //node中讀取文件,第一個參數是路徑,第二個是編碼,第三個是回調,錯誤在先 fs.readFile(filename, 'utf-8', (err, data) => { if(err) resolver.reject(err) resolver.resolve(data) }) }) }
//演示一下調用 // readFile調用返回的是Task函子,調用要用run方法 readFile('package.json') .run() // 現在沒有對resolve進行處理,可以使用task的listen去監聽獲取的結果 // listen傳一個對象,onRejected是監聽錯誤結果,onResolved是監聽正確結果 .listen({ onRejected: (err) => { console.log(err) }, onResolved: (value) => { console.log(value) } })
/* { "name": "Functor", "version": "1.0.0", "description": "", "main": "either.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC", "dependencies": { "folktale": "^2.3.2", "lodash": "^4.17.20" } } / ```
Demo:在package.json
文件中提取一下version
字段
```javascript const { task } = require('folktale/concurrency/task') const fs = require('fs') const { split, find } = require('lodash/fp') // 2.0中是一個函數,函數返回一個函子對象 // 1.0中是一個類
//讀取文件 function readFile (filename) { // task傳遞一個函數,參數是resolver // resolver裏面有兩個參數,一個是reject失敗的時候執行的,一個是resolve成功的時候執行的 return task(resolver => { //node中讀取文件,第一個參數是路徑,第二個是編碼,第三個是回調,錯誤在先 fs.readFile(filename, 'utf-8', (err, data) => { if(err) resolver.reject(err) resolver.resolve(data) }) }) }
//演示一下調用 // readFile調用返回的是Task函子,調用要用run方法 readFile('package.json') //在run之前調用map方法,在map方法中會處理的拿到文件返回結果 // 在使用函子的時候就沒有必要想的實現機制 .map(split('\n')) .map(find(x => x.includes('version'))) .run() // 現在沒有對resolve進行處理,可以使用task的listen去監聽獲取的結果 // listen傳一個對象,onRejected是監聽錯誤結果,onResolved是監聽正確結果 .listen({ onRejected: (err) => { console.log(err) }, onResolved: (value) => { console.log(value) // "version": "1.0.0", } })
```
Monad函子
IO函子的嵌套問題
Monad函子可以解決IO函子的嵌套問題,IO函子的嵌套問題如下:
```javascript const fp = require('lodash/fp') const fs = require('fs')
class IO { static of (value) { return new IO(() => { return value }) } constructor (fn) { this._value = fn }
map(fn) { return new IO(fp.flowRight(fn, this._value)) } }
//讀取文件函數 let readFile = (filename) => { return new IO(() => { //同步獲取文件 return fs.readFileSync(filename, 'utf-8') }) }
//打印函數 // x是上一步的IO函子 let print = (x) => { return new IO(()=> { console.log(x) return x }) }
// 組合函數,先讀文件再打印 let cat = fp.flowRight(print, readFile) // 調用 // 拿到的結果是嵌套的IO函子 IO(IO(x)) let r = cat('package.json') console.log(r) // IO { _value: [Function] } console.log(cat('package.json')._value()) // IO { _value: [Function] } // IO { _value: [Function] } console.log(cat('package.json')._value()._value()) // IO { _value: [Function] } /* * { "name": "Functor", "version": "1.0.0", "description": "", "main": "either.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC", "dependencies": { "folktale": "^2.3.2", "lodash": "^4.17.20" } } / ```
上面遇到多個IO函子嵌套的時候,那麼_value就會調用很多次,這樣的調用體驗很不好。所以進行優化。
什麼是Monad函子
-
Monad函子是可以變扁的
Pointed
函子,用來解決IO函子嵌套問題,IO(IO(x))
-
一個函子如果具有
join
和of
兩個方法並遵守一些定律就是一個Monad
實現一個Monad函子
```javascript const fp = require('lodash/fp') const fs = require('fs')
class IO { static of (value) { return new IO(() => { return value }) } constructor (fn) { this._value = fn }
map(fn) { return new IO(fp.flowRight(fn, this._value)) }
join () { return this._value() }
// 同時調用map和join方法 flatMap (fn) { return this.map(fn).join() } }
let readFile = (filename) => { return new IO(() => { return fs.readFileSync(filename, 'utf-8') }) }
let print = (x) => { return new IO(()=> { console.log(x) return x }) }
let r = readFile('package.json')
.flatMap(print)
.join()
// 執行順序
/*
* readFile讀取了文件,然後返回了一個IO函子
* 調用flatMap是用readFile返回的IO函子調用的
* 並且傳入了一個print函數參數
* 調用flatMap的時候,內部先調用map,當前的print和this._value進行合併,合併之後返回了一個新的函子
* (this._value就是readFile返回IO函子的函數:
* () => {
return fs.readFileSync(filename, 'utf-8')
}
* )
* flatMap中的map函數執行完,print函數返回的一個IO函子,裏面包裹的還是一個IO函子
* 下面調用join函數,join函數就是調用返回的新函子內部的this._value()函數
* 這個this._value就是之前print和this._value的組合函數,調用之後返回的就是print的返回結果
* 所以flatMap執行完畢之後,返回的就是print函數返回的IO函子
* /
r = readFile('package.json') // 處理數據,直接在讀取文件之後,使用map進行處理即可 .map(fp.toUpper) .flatMap(print) .join()
// 讀完文件之後想要處理數據,怎麼辦? // 直接在讀取文件之後調用map方法即可
/* * { "NAME": "FUNCTOR", "VERSION": "1.0.0", "DESCRIPTION": "", "MAIN": "EITHER.JS", "SCRIPTS": { "TEST": "ECHO \"ERROR: NO TEST SPECIFIED\" && EXIT 1" }, "KEYWORDS": [], "AUTHOR": "", "LICENSE": "ISC", "DEPENDENCIES": { "FOLKTALE": "^2.3.2", "LODASH": "^4.17.20" } } / ```
Monad函子小結
什麼是Monad函子?
答:具有靜態的IO
方法和join
方法的函子。
什麼時候使用Monad?
答:
-
當一個函數返回一個函子的時候,我們就要想到monad,monad可以幫我們解決函子嵌套的問題。
-
當我們想要返回一個函數,這個函數返回一個值,這個時候可以調用****方法
-
當我們想要去合併一個函數,但是這個函數返回一個函子,這個時候我們要用
flatMap
方法
{完}
- 用ChatGPT學Nginx是一種什麼體驗
- 【好物分享】分享給前端開發的28個資源(網站、軟件、插件),簡直是提高效率必備
- Vite3.0都來了,你還捲動嗎?(Vite3.0新特性一覽)
- 【好物分享】在命令行讀Markdown,這個感覺太舒服了
- 從0開始使用pnpm構建一個Monorepo方式管理的demo
- 我畫了5張腦圖可以讓你快速入門TypeScript
- 我看着MDN文檔,手寫了幾個數組實例方法
- 淺談JavaScript中的特殊函數
- 如何通過SSH配合VSCode收穫超舒適的遠程開發體驗
- CSS的calc函數不會還有人沒有用吧
- 【戲玩算法】12-圖
- 誰説前端不能搞紅黑樹,用這55張圖拿JS一起手撕紅黑樹
- 簡單總結了10個JavaScript代碼優化小tips
- NaiveUI中看起來沒啥用的組件(文字漸變)實現原來這麼簡單
- 面試官讓我用Flex寫色子佈局,我直接給寫了6個
- Vue3 TS Vite NaiveUI搭建一個項目骨架
- 用一萬多字從頭到尾介紹【函數式編程】
- 這8張腦圖幾乎概括了所有的佈局方案,確定不看看嗎?
- 【戲玩算法】07-字典
- 還在console.log一把梭嗎?console還有其他騷操作