1. 程式人生 > 其它 >函數語言程式設計與柯里化-筆記

函數語言程式設計與柯里化-筆記

技術標籤:拉勾大前端訓練營前端大前端函數語言程式設計

00-接上篇

在上一篇中主要記錄了函式柯里化的基本概念和手動實現,當然如果在每一個專案中都手動實現一遍柯里化顯然是不方便的。其次,函式柯里化在函數語言程式設計中具體有哪些應用模式呢?本篇將主要講述柯里化的使用和函數語言程式設計的應用。

01-Lodash中的柯里化功能

lodash是一個一致性、模組化、高效能的 JavaScript 實用工具庫。在其內部將我們生產和開發中的許多常用功能抽象成了各種函式以方便呼叫(雖然他們不一定都是純函式),其中便提供了currycurryRight方法,這一方法可以幫助我們將函式柯里化。其中curry方法與我們自己實現的函式使用方法差異不多,如下:

var abc = function(a, b, c) {
  return [a, b, c];
};

var curried = _.curry(abc);

curried(1)(2)(3);
// => [1, 2, 3]

curried(1, 2)(3);
// => [1, 2, 3]

curried(1, 2, 3);
// => [1, 2, 3]

// Curried with placeholders.
curried(1)(_, 3)(2);

// => [1, 2, 3]

curryRight除了 會生成柯里化之後的函式之外,其傳遞引數的順序是由後向前。

02-初步總結柯里化

至此,單純關於柯里化的內容已經完畢,柯里化的優點和作用已然明瞭:

柯里化可以通過閉包的方式實現生成一個“快取了一部分引數的函式”從而使我們能夠無壓力的抽象細粒度的函式而不必擔心傳入的引數過多的不便。

但是柯里化顯然不止是開發上多傳一個或者少傳一個引數的問題,柯里化的核心其實是將一個多元函式轉換成一個一元函式,而為什麼要組合成一元函式呢?答案是:函式組合

03-函式組合

首先假設我們需要設計這樣一個函式,這個函式的目的是獲取陣列的最後一個元素並且轉換為大寫字母,如果應用我們之前所涉及的柯里化和純函式,我們理所當然的會寫出這樣的程式碼:

_.toUpper(_.first(_.
reverse(array)))

這種程式碼雖然可以正常並且順利的獲得我們想要的結果,但是這種“洋蔥程式碼”的層層巢狀如果層級太多就會不利於程式碼的編寫和維護。所以在這種情況我們就不再適合巢狀使用,而是使用“函式組合”:

函式組合可以讓我們把細粒度的函式重新組合生成一個新的函式

函式組合的思想是一種由從內到外編寫格式到依次執行思路的轉變,他有點像pipeline,但是完全不同,不過這並不能阻止你將他想像成一個管道:

如果說一個純函式是一個完整的管道,接受一個輸入,並提供一個輸出,就像:

那麼使用純函式的函式組合就像是把一個複雜的多個管道轉變成一個個連線在一起的細分小管道:

到這之前的鋪墊就都講得通了,之所以要將函式進行柯里化,是因為需要將多元函式轉變為一元函式。而需要一元純函式的原因,就是在具體組合應用的時候方便進行函式組合。函式組合思想的便利之處就在於,我們可以放心的儘可能細粒度的抽象函式而不必擔心呼叫時過於複雜的問題。我們在應用的時候只要拿出足夠細粒度的函式“積木”進行進一步的功能通過組合函式進行整合即可(需要注意的是組合函式的執行順序是從右到左)。如:

// 組合函式
function compose (f, g) {
return function (x) {
return f(g(x))
}
}
function first (arr) {
return arr[0]
}
function reverse (arr) {
return arr.reverse()
}
// 從右到左執行
let last = compose(first, reverse)
console.log(last([1, 2, 3, 4]))

看過程式碼,相信“從右到左”這個似乎很反常規的規則的原因也已經明瞭了,雖然我們傳參的順序是(f, g),但是隻要想像一下內部的“洋蔥程式碼”,就可得知其實洋蔥的最內部是”g“,是內部的函式先執行。

而這樣的解決方式顯然還是不夠“優雅”,你可能會發現其實只是把洋蔥程式碼換了個地方寫了而已,而且這種函式組合只能處理兩個函式的情況。如果我們需要使用多個函式的組合又該怎麼辦呢?Lodash為我們提供瞭解決方案flow()flowRight(),其中flowRight便是從右到左執行的組合方式。這種方式在實際應用中會使用的更多一點(因為他能更好的幫助我們去想像其中的洋蔥函式???)。如:

const _ = require('lodash')
const toUpper = s => s.toUpperCase()
const reverse = arr => arr.reverse()
const first = arr => arr[0]
const f = _.flowRight(toUpper, first, reverse)
console.log(f(['one', 'two', 'three']))

可以看出我們很優雅的將三個細粒度的純函式進行了組合形成了一個全新的函式f(),比洋蔥程式碼要好維護得多了。

以下是面試專用手寫一切時間!!!

在面試的的時候如果面試官問起函式組合是如何實現的,我們可以給出這樣一個思路:

  • 獲取傳入所有引數的偽陣列

  • 將其反轉(因為我們需要從右向左依次執行)

  • 依次組合函式,並將前一個函式的返回值作為後一個函式的引數

  • 返回生成的函式

如果面試官問:那麼用程式碼如何實現呢,我們便可以僅用一行程式碼展現自己優秀的程式設計水平(並不),如:

const compose = (...fns) => value => fns.reverse().reduce((acc, fn) =>
fn(acc), value)

在這行程式碼中我們依然使用了…引數的這樣的剩餘引數用法以獲取所有的引數構成的偽陣列。之後我們簡單的將其簡單的reverse()。然後呼叫了reduce()方法。(這裡值得好好說一下)

reduce函式是一個累加器,它接受一個函式作為引數,並且返回陣列中每一個元素通過這個函式進行累加之後的值。這樣說可能比較抽象,我們可以用陣列求和來舉個栗子。

var numbers = [65, 44, 12, 4];

function getSum(total, num) {
    return total + num;

}

function myFunction(item) {
    document.getElementById("demo").innerHTML = numbers.reduce(getSum);

}

reduce函式會將累加中間結果作為第一個引數,陣列項作為第二個引數傳入提供的函式,從而返回最終結果。在這個案例中我們會對這個陣列中的每一項進行求和。

回到我們手寫的compose函式,我們的函式接受一系列引數,返回的內容是一個接受一個value引數的函式,函式的內部會累加執行當前函式並且將之前函式的結果作為引數,當然還會傳入我們傳入的value作為初始引數。翻譯成更好理解的方式就是:

const compose = (...fns) => value => fns.reverse().reduce((acc, fn) =>
fn(acc), value)


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

你學廢了嗎?

04-除錯組合函式

除錯組合函式…就是在呼叫的一串函式中間插一個柯里化過的額外函式,然後將接收到的中間引數原樣返回傳遞下去就可以了…(其實可能不如打個斷點?)

示例程式碼

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'))

05-Point free

其實前面幾千字的鋪墊都是為了這一點…

Point free 程式設計顧名思義就是他可以省去一些東西,而被free的point是什麼呢?你可以這樣理解:

如果我們清楚的知道一個公式的計算過程,那麼我們即使不提供任何資料,就可以表達出這個公式,並且確信他能達到預期的結果。

而這就需要純函式,柯里化,函式組合等概念作為前提:

  • 如果一個函式會產生副作用,那麼就無法確定預期結果(是否為純函式)

  • 如果一個函式是多元函式,那麼就必須額外提供引數(不需要提供任何資料)

  • 如果一個函式可以被組合,那麼他就可以用來表示程式的計算過程(表達出一個公式)

PointFree這一特點通常用來封裝一個底層函式,這樣我們無需瞭解其內部的運作流程,只要呼叫這個函式就可以了,比如:

// Hello World => hello_world
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)
console.log(f('Hello World'))


const fp = require('lodash/fp')
const firstLetterToUpper = fp.flowRight(join('. '),
fp.map(fp.flowRight(fp.first, fp.toUpper)), split(' '))
console.log(firstLetterToUpper('world wild web'))
// => W. W. W

06-函數語言程式設計相關庫

lodash/fp

  • lodash 的 fp 模組提供了實用的對函數語言程式設計友好的方法

  • 提供了不可變 auto-curried iteratee-first data-last 的方法

(意思就是所有的函式都已經柯里化過,如果同時傳參後傳資料)

// lodash 模組
const _ = require('lodash')
_.map(['a', 'b', 'c'], _.toUpper)
// => ['A', 'B', 'C']
_.map(['a', 'b', 'c'])
// => ['a', 'b', 'c']
_.split('Hello World', ' ')
// lodash/fp 模組
const fp = require('lodash/fp')
fp.map(fp.toUpper, ['a', 'b', 'c'])
fp.map(fp.toUpper)(['a', 'b', 'c'])
fp.split(' ', 'Hello World')
fp.split(' ')('Hello World')
//-------------------------------------
const fp = require('lodash/fp')
const f = fp.flowRight(fp.join('-'), fp.map(_.toLower), fp.split(' '))
console.log(f('NEVER SAY DIE'))

本系列/專欄為拉勾教育-大前端高薪訓練營學習筆記,內容為本系列課程的講授內容、亮點題目分析、重點難點的總結、以及個人的體會。個人感覺拉勾教育比體驗過的其他教育平臺要更好一點。老師講授的內容比較全面,相對於自學可以節省很多不必要的走彎路的時間,可以更快的使自己在技術上系統的有所提高。同時隨堂測的題目也很用代表性,老師跟進解答很快,推薦和我一樣在自學路上遇到瓶頸或者找不到進一步學習方向的同學嘗試一下。