函數語言程式設計

地靈 發表於 2022-05-15

函數語言程式設計

為什麼要學習函數語言程式設計以及什麼是函數語言程式設計,包括函數語言程式設計的特性(純函式、柯里化、函式組合等)
函數語言程式設計就是利用純函式來實現一些細粒度的函式,再通過函式的組合把這些細粒度的函式組合成為功能更強大的函式

為什麼要學習函數語言程式設計

函數語言程式設計是非常古老的一個概念,早於第一臺計算機的誕生。有興趣的同學可以自行搜尋,那我們為什麼還要學習函數語言程式設計,基於以下幾點來簡單說明下:
  • 函數語言程式設計是隨著 React 的流行受到越來越多的關注
  • Vue 3也開始擁抱函數語言程式設計
  • 函數語言程式設計可以拋棄 this
  • 打包過程中可以更好的利用 tree shaking 過濾無用程式碼
  • 方便測試、方便並行處理
  • 有很多庫可以幫助我們進行函式式開發:lodash、underscore、ramda
函數語言程式設計(Functional Programming, FP),FP 是程式設計正規化之一,我們常聽說的程式設計正規化還有程式導向程式設計、物件導向程式設計。函數語言程式設計的思維方式:把現實世界的事物和事物之間的聯絡抽象到程式世界(對運算過程進行抽象),用一段程式碼來簡單演示一下
// 求和
// 非函式式
let num1 = 1
let num2 = 2
let sum = num1 + num2
console.log(sum)

// 函式式
function add (n1, n2) {
    return n1 + n2
}
let sum = add(1, 2)
console.log(sum)
// 函式式這個例子就是表示對運算過程進行抽象,另外函數語言程式設計中的函式指的不是程式中的函式(方法),而是數學中的函式即對映關係,並且要求相同的輸入始終要有相同的輸出

函數語言程式設計的前置知識

一、函式是一等公民幾個點:
  • 函式可以儲存在變數中
  • 函式作為引數
  • 函式作為返回值
二、高階函式兩個定義:
  • 可以把函式作為引數傳遞給另一個函式(比如陣列的 forEach 方法,讓我們舉個例子)
// 模擬 forEach
function forEach(arr, fn) {
    for(let i = 0; i < arr.length; i++) {
        fn(arr[i])
    }
}
// 測試
let arr = [1, 2, 3, 4]
forEach(arr, function (item) {
    console.log(item)
})
// 結果 1 2 3 4
  • 函式作為返回值
function makeFn () {
    let msg = 'Hello'
    return function () {
        console.log(msg)
    }
}

const fn = makeFn()
fn()
// 結果 Hello

高階函式的意義主要是抽象通用的問題,比如 forEach 我們不需關注迴圈的具體實現,只需要做我們要達成的事情就行。
三、閉包:
  • 可以在另一個作用域中呼叫一個函式的內部函式並訪問到該函式的作用域中的成員。
  • 本質:函式在執行的時候會放到一個執行棧上當函式執行完畢之後會從執行棧上移除,但是
    堆上的作用域成員因為被外部引用不能釋放,因此內部函式依然可以訪問外部函式的成員

函數語言程式設計的核心概念

前面介紹的是基礎,接下來講述第一個核心,純函式
純函式講的是:相同的輸入永遠會得到相同的輸出,而且沒有任何可觀察的副作用。,比如說陣列的操作方法 slice 和 splice:
  • slice 返回陣列中的指定部分,不會改變原陣列(純函式)
  • splice 對陣列進行操作返回該陣列,會改變原陣列
純函式的好處:可快取,因為純函式對相同的輸入始終有相同的結果,所以可以把純函式的結果快取起來
function memoize(f) {
    let cache = {}
    return function () {
        let key = JSON.stringify(arguments)
        cache[key] = cache[key] || f.apply(f, arguments)
        return cache[key]
    }
}
如果純函式依賴於外部的狀態就無法保證輸出相同,就會帶來副作用。
第二個是函式柯里化:用一段簡單的程式碼來表示
function getSum (a, b, c) {
    return a + b + c
}

function curry (func) {
    return function curriedFn(...args) {
        // 判斷實參和形參的個數
        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))
console.log(curried(1)(2, 3))
console.log(curried(1, 2)(3))
// 6 6 6
  • 柯里化可以讓我們給一個函式傳遞較少的引數得到一個已經記住了某些固定引數的新函式
  • 這是一種函式引數的快取
  • 讓函式變的更靈活,讓函式的粒度更小
  • 可以把多元函式轉換成一元函式,可以組合使用函式產生強大的功能
第三個是函式組合:純函式和柯里化很容易寫出洋蔥程式碼h(g(f(x))),比如說獲取陣列的最後一個元素再轉換成大寫字母,xx.toUpper(xx.first(xx.reverse(array))),而函式組合可以讓我們把細粒度的函式重新組合生成一個新的函式。概念如下:
  • 如果一個函式要經過多個函式處理才能得到最終值,這個時候可以把中間過程的函式合併成一個函式
  • 函式就像是資料的管道,函式組合就是把這些管道連線起來,讓資料穿過多個管道形成最終結果
  • 函式組合預設是從右到左執行
  • 舉個例子
// 組合函式
function compose (f, g) {
    return function(value) {
        return f(g(value))
    }
}

function reverse (array) {
    return array.reverse()
}

function first (array) {
    return array[0]
}

const last = compose(first, reverse)
console.log(last([1, 2, 3, 4]))
// 結果 4
雖然這個程式碼比較麻煩,但是這些輔助函式可以任意的組合。所以函數語言程式設計可以最大的重用