程式設計正規化 —— 函數語言程式設計入門
該系列會有 3 篇文章,分別介紹什麼是函數語言程式設計、剖析函數語言程式設計庫、以及函數語言程式設計在 React 中的應用,歡迎關注我的 blog
指令式程式設計和宣告式程式設計
拿泡茶這個事例進行區分指令式程式設計和宣告式程式設計
- 指令式程式設計
1.燒開水(為第一人稱)
2.拿個茶杯
3.放茶葉
4.沖水
- 宣告式程式設計
1.給我泡杯茶(為第二人稱)
舉個 demo
// 指令式程式設計 const convert = function(arr) { const result = [] for (let i = 0; i < arr.length; i++) { result[i] = arr[i].toLowerCase() } return result } // 宣告式程式設計 const convert = function(arr) { return arr.map(r => r.toLowerCase()) }
什麼是函數語言程式設計
函數語言程式設計是宣告式程式設計的正規化。在函數語言程式設計中資料在由純函式組成的管道中傳遞。
函數語言程式設計可以用簡單如 交換律、結合律、分配律
的數學之法來幫我們簡化程式碼的實現。
它具有如下一些特性:
- 純粹性: 純函式不改變除當前作用域以外的值;
// 反面示例 let a = 0 const add = (b) => a = a + b // 兩次 add(1) 結果不一致 // 正確示例 const add = (a, b) => a + b
- 資料不可變性: Immutable
// 反面示例 const arr = [1, 2] const arrAdd = (value) => { arr.push(value) return arr } arrAdd(3) // [1, 2, 3] arrAdd(3) // [1, 2, 3, 3] // 正面示例 const arr = [1, 2] const arrAdd = (value) => { return arr.concat(value) } arrAdd(3) // [1, 2, 3] arrAdd(3) // [1, 2, 3]
在後記 1 中對陣列字串方法是否對原值有影響作了整理
- 函式柯里化: 將多個入參的函式轉化為一個入參的函式;
const add = a => b => c => a + b + c add(1)(2)(3)
- 偏函式: 將多個入參的函式轉化成兩部分;
const add = a => (b, c) => a + b + c add(1)(2, 3)
- 可組合: 函式之間能組合使用
const add = (x) => x + x const mult = (x) => x * x const addAndMult = (x) => add(mult(x))
柯里化(curry)
如下是一個加法函式:
var add = (a, b, c) => a + b + c add(1, 2, 3) // 6
假如有這樣一個 curry
函式, 用其包裝 add
函式後返回一個新的函式 curryAdd
, 我們可以將引數 a、b
進行分開傳遞進行呼叫。
var curryAdd = curry(add) // 以下輸出結果都相同 curryAdd(1, 2, 3) // 6 curryAdd(1, 2)(3) // 6 curryAdd(1)(2)(3) // 6 curryAdd(1)(2, 3) // 6
動手實現一個 curry 函式
核心思路: 若傳進去的引數個數未達到 curryAdd
的個數,則將引數快取在閉包變數 lists 中:
function curry(fn, ...args) { const length = fn.length let lists = args || [] let listLen return function (..._args) { lists = [...lists, ..._args] listLen = lists.length if (listLen < length) { const that = lists lists = [] return curry(fn, ...that) } else if (listLen === length) { const that = lists lists = [] return fn.apply(this, that) } } }
程式碼組合(compose)
現在有 toUpperCase
、 reverse
、 head
三個函式, 分別如下:
var toUpperCase = (str) => str.toUpperCase() var reverse = (arr) => arr.reverse() var head = (arr) => arr[0]
接著使用它們實現將陣列末位元素大寫化輸出, 可以這樣做:
var reverseHeadUpperCase = (arr) => toUpperCase(head(reverse(arr))) reverseHeadUpperCase(['apple', 'banana', 'peach']) // "PEACH"
此時在構建 reverseHeadUpperCase
函式的時候, 必須手動宣告傳入引數 arr, 是否能提供一個 compose
函式讓使用者更加友好的使用呢? 類似如下形式:
var reverseHeadUpperCase = compose(toUpperCase, head, reverse) reverseHeadUpperCase(['apple', 'banana', 'peach']) // "PEACH"
此外 compose
函式符合 結合律
, 我們可以這樣子使用:
compose(compose(toUpperCase, head), reverse) compose(toUpperCase, compose(head, reverse))
以上兩種寫法與 compose(toUpperCase, head, reverse)
的效果完全相同, 都是依次從右到左執行傳參中的函式。
此外 compose
和 map
一起使用時也有相關的結合律, 以下兩種寫法效果相等
compose(map(f), map(g)) map(compose(f, g))
動手實現一個 compose 函式
程式碼精華集中在一行之內, 其為眾多開源庫(比如 Redux) 所採用。
var compose = (...args) => (initValue) => args.reduceRight((a, c) => c(a), initValue)
範疇論
範疇論是數學中的一個分支。可以將範疇理解為一個容器, 把原來對值的操作,現轉為對容器的操作。如下圖:
學習函數語言程式設計就是學習各種函子的過程。
函數語言程式設計中, 函子(Functor)
是實現了 map
函式的容器, 下文中將函子視為範疇,模型可表示如下:
class Functor { constructor(value) { this.value = value } map(fn) { return new Functor(fn(this.value)) } }
但是在函數語言程式設計中, 要避免使用 new
這種面向物件的程式設計方式, 取而代之對外暴露了一個 of
的介面, 也稱為 pointed functor
。
Functor.of = value => new Functor(value)
Maybe 函子
Maybe 函子
是為了解決 this.value
為 null 的情形, 用法如下:
Maybe.of(null).map(r => r.toUpperCase()) // null Maybe.of('m').map(r => r.toUpperCase())// Maybe {value: "M"}
實現程式碼如下:
class Maybe { constructor(value) { this.value = value } map(fn) { return this.value ? new Maybe(fn(this.value)) : null } } Maybe.of = value => new Maybe(value)
Either 函子
Either 函子
是為了對應 if...else...
的語法, 即 非左即右
。因此可以將之拆分為 Left
和 Right
兩個函子, 它們的用法如下:
Left.of(1).map(r => r + 1)// Left {value: 1} Right.of(1).map(r => r + 1) // Right {value: 2}
Left 函子
實現程式碼如下:
class Left { constructor(value) { this.value = value } map(fn) { return this } } Left.of = value => new Left(value)
Right 函子
實現程式碼如下(其實就是上面的 Functor
):
class Right { constructor(value) { this.value = value } map(fn) { return new Right(fn(this.value)) } } Right.of = value => new Right(value)
具體 Either
函式只是對呼叫 Left 函子
或 Right 函子
作一層篩選, 其接收 f
、 g
兩個函式以及一個函子( Left or Right
)
var Either = function(f, g, functor) { switch(functor.constructor) { case 'Left': return f(functor.value) case 'Right': return g(functor.value) default: return f(functor.value) } }
使用 demo:
Either((v) => console.log('left', v), (v) => console.log('def', v), left)// left 1 Either((v) => console.log('rigth', v), (v) => console.log('def', v), rigth) // rigth 2
Monad 函子
函子會發生巢狀, 比如下面這樣:
Functor.of(Functor.of(1)) // Functor { value: Functor { value: 1 } }
Monad 函子
對外暴露了 join
和 flatmap
介面, 呼叫者從而可以扁平化巢狀的函子。
class Monad { constructor(value) { this.value = value } map(fn) { return new Monad(fn(this.value)) } join() { return this.value } flatmap(fn) { return this.map(fn).join() } } Monad.of = value => new Monad(value)
使用方法:
// join Monad.of(Monad.of(1).join()) // Monad { value: 1 } Monad.of(Monad.of(1)).join() // Monad { value: 1 } // flatmap Monad.of(1).flatmap(r => r + 1)// 2
Monad 函子可以運用在 I/O 這種不純的操作上將之變為純函式的操作,目前比較懵懂,日後補充。
後記 1: 陣列字串方法小結(是否對原值有影響)
不會對原陣列有影響的方法
slice
var test = [1, 2, 3] var result = test.slice(0, 1) console.log(test)// [1, 2, 3] console.log(result) // [1]
concat
var test = [1, 2, 3] var result = test.concat(4) console.log(test)// [1, 2, 3] console.log(result) // [1, 2, 3, 4]
對原陣列有影響的方法
splice(這個需要特別記一下)
var test = [1, 2, 3] var result = test.splice(0, 1) console.log(test)// [2, 3] console.log(result) // [1]
sort
var arr = [2, 1, 3, 4] arr.sort((r1, r2) => (r1 - r2)) console.log(arr) // [1, 2, 3, 4]
reverse
var test = [1, 2, 3] var result = test.reverse() console.log(test)// [3, 2, 1] console.log(result) // [3, 2, 1]
push/pop/unshift/shift
var test = [1, 2, 3] var result = test.push(4) console.log(test)// [1, 2, 3, 4] console.log(result) // 4
不會對原字串造成影響的方法
substr/substring/slice
// substr var test = 'abc' var result = test.substr(0, 1) console.log(test)// 'abc' console.log(result) // a // substring var test = 'abc' var result = test.substring(0, 1) console.log(test)// 'abc' console.log(result) // a // slice var test = 'abc' var result = test.slice(0, 1) console.log(test)// 'abc' console.log(result) // a