以 JavaScript 編程語言介紹函數式編程的主要概念。

函數式編程介紹

函數式編程(FP)是一種具有特定技術的編程範例。

在編程語言中,你會找到純函數式編程語言以及支持函數式編程技術的編程語言。

Haskell、Clojure 和 Scala 是一些最受歡迎的純函數式編程語言。

支持函數式編程技術的流行編程語言包括 JavaScript、Python、Ruby 等等。

函數式編程並不是一個新概念,實際上它的根源可以追溯到 1930 年代的 Lambda 演算,並影響了許多編程語言。

近年來,函數式編程一直在不斷發展,所以現在是學習它的完美時機。

在本課程中,我將使用 JavaScript 代碼示例介紹函數式編程的主要概念。

一級函數

在函數式編程語言中,函數是一級公民。

可以分配給變量

const f = (m) => console.log(m)
f('Test')

由於函數可以分配給變量,它們可以添加到對象中:

const obj = {
 f(m) {
 console.log(m)
 }
}
obj.f('Test')

也可以添加到數組中:

const a = [
 m => console.log(m)
]
a[0]('Test')

可以用作其他函數的參數

const f = (m) => () => console.log(m)
const f2 = (f3) => f3()
f2(f('Test'))

可以由函數返回

const createF = () => {
 return (m) => console.log(m)
}
const f = createF()
f('Test')

高階函數

接受函數作為參數或返回函數的函數稱為高階函數

在 JavaScript 標準庫中的例子包括 Array.map()Array.filter()Array.reduce(),我們稍後會看到它們的用法。

聲明性編程

你可能聽說過「聲明性編程」這個詞。

讓我們將這個詞放入上下文中。

「聲明性」的對立面是 命令式

一個命令式的方法是告訴機器(一般而言)它需要執行哪些步驟才能完成一項工作。

一種聲明性的方法是告訴機器你需要做什麼,然後讓它找到解決方案的細節。

當你有足夠的抽象層次時,你開始思考聲明性,停止思考低級構造,更多地從高級UI層次進行思考。

有人可能會認為C編程比彙編編程更具有聲明性,這是正確的。

HTML 是聲明性的,所以如果你從 1995 年開始使用 HTML,你實際上已經在構建聲明性的 UI20多年了。

JavaScript 可以以命令式或聲明性的編程方式進行編程。

例如,聲明性的編程方法是避免使用 循環,而是使用功能編程結構,如 mapreducefilter,因為你的程序更抽象,更少關注告訴機器每個處理步驟的細節。

不變性

在函數式編程中,數據永遠不會改變。數據是不可變的

變量永遠不會被更改。要更新它的值,你需要創建一個新的變量。

代替更改數組,要添加新項目,你需要創建一個新的數組,將舊數組與新項目連接起來。

在更改對象之前,將對象進行複製。

const

這就是為什麼 ES2015 中廣泛使用 const 的原因。ES2015 擁抱了函數式編程概念,const 可以強制變量的不可變性。

Object.assign()

ES2015 還提供了 Object.assign(),它是創建對象的關鍵:

const redObj = { color: 'red' }
const yellowObj = Object.assign({}, redObj, {color: 'yellow'})

concat()

在 JavaScript 中,要將項目添加到數組中,我們通常使用數組上的 push() 方法,但該方法會改變原始數組,因此它不能用於函數式編程。

我們可以使用 concat() 方法:

const a = [1, 2]
const b = [1, 2].concat(3)
// b = [1, 2, 3]

或者我們可以使用展開運算符

const c = [...a, 3]
// c = [1, 2, 3]

filter()

從數組中刪除項目時,也是同樣的操作:不要使用 pop()splice(),它們會修改原始數組,可以使用 array.filter()

const d = a.filter((v, k) => k < 1)
// d = [1]

純度

純函數

  • 永遠不會更改通過引用傳遞給它的任何參數(在 JS 中,對象和數組):它們應被視為不可變。當然,它可以更改按值複製的任何參數
  • 純函數的返回值不受其它任何東西的影響,只受其輸入參數的影響:傳遞相同的參數始終返回相同的輸出
  • 在執行過程中,純函數不會更改其外部的任何東西

數據轉換

由於不可變性是函數式編程的重要概念和基礎,你可能會問數據如何更改。

簡單:通過創建副本來更改數據

特別是,函數通過返回數據的新副本來更改數據。

執行此操作的核心函數是 mapreduce

Array.map()

在數組上調用 Array.map() 會在原始數組的每個項目上執行一個函數,並創建一個新數組,其值為該函數的結果:

const a = [1, 2, 3]
const b = a.map((v, k) => v \* k)
// b = [0, 2, 6]

Array.reduce()

在數組上調用 Array.reduce() 可以將該數組轉換為其他任意類型,包括純量、函數、布爾值和對象。

你需要傳遞一個處理結果的函數和一個起始點:

const a = [1, 2, 3]
const sum = a.reduce((partial, v) => partial + v, 0)
// sum = 6
const o = a.reduce((obj, k) => { obj[k] = k; return obj }, {})
// o = {1: 1, 2: 2, 3: 3}

遞歸

遞歸是函數式編程的一個重點。當一個函數調用自身時,稱為 遞歸函數

遞歸的典型例子是費波那契數列(N = (N-1 + N-2))計算,在這裡我們使用完全低效(但好讀)的解決方案:

var f = (n) => n <= 1 ? 1 : f(n-1) + f(n-2)

組合

組合是函數式編程的另一個重要主題,這正是將它放入“關鍵主題”列表的好原因。

組合是通過結合較簡單的函數而生成高階函數的過程

在純 JS 中進行組合

在純 JavaScript 中,組合函數的一種常見方法是將它們鏈接在一起:

obj.doSomething()
 .doSomethingElse()

或者,也非常常用的是通過將函數執行傳遞到函數中:

obj.doSomething(doThis())

使用 lodash 進行組合

更一般地,組合是將許多函數打包在一起執行更複雜操作的過程。

lodash/fp 提供了 compose 的實現:我們執行一個函數列表,從一個參數開始,每個函數從前一個函數的返回值中繼承參數。請注意,我們不需要在任何地方存儲中間值。

import { compose } from 'lodash/fp'

const slugify = compose(
 encodeURIComponent,
 join('-'),
 map(toLowerCase),
 split(' ')
)

slufigy('Hello World') // hello-world