函數式編程,真香

最開始接觸函數式編程的時候是在小米工做的時候,那個時候看老大之前寫的代碼各類 compose,而後一些 ramda 的一些工具函數,看着很吃力,而後極力吐槽函數式編程,如今回想起來,那個時候的本身真的是見識短淺,只想說,'真香'。javascript

最近在研究函數式編程,真的是在學習的過程當中感受本身的思惟提高了不少,抽象能力大大的提升了,讓我深深的感覺到了函數式編程的魅力。因此我打算後面用 5 到 8 篇的篇幅,詳細的介紹一下函數式編程的思想,基礎、如何設計、測試等。前端

今天這篇文章主要介紹函數式編程的思想。java

  • 函數式編程有用嗎?
  • 什麼是函數式編程?
  • 函數式編程的優勢。

面向對象編程(OOP)經過封裝變化使得代碼更易理解。 函數式編程(FP)經過最小化變化使得代碼更易理解。 -- Michacel Feathers(Twitter)數據庫

衆所周知 JavaScript 是一種擁有不少共享狀態的動態語言,慢慢的,代碼就會積累足夠的複雜性,變得笨拙難以維護。面向對象設計能幫咱們在必定程度上解決這個問題,可是還不夠。編程

因爲有不少的狀態,因此處理數據流和變化的傳遞顯得尤其重要,不知道大家知道響應式編程與否,這種編程範式有助於處理 JavaScript 的異步或者事件響應。總之,當咱們在設計應用程序的時候,咱們應該考慮是否遵照了如下的設計原則。redux

  • 可擴展性--我是否須要不斷地重構代碼來支持額外的功能?
  • 易模塊化--若是我更改了一個文件,另外一個文件是否會受到影響?
  • 可重用性--是否有不少重複的代碼?
  • 可測性--給這些函數添加單元測試是否讓我糾結?
  • 易推理性--我寫的代碼是否非結構化嚴重並難以推理?

我這能這麼跟你說,一旦你學會了函數式編程,這些問題迎刃而解,原本函數式編程就是這個思想,一旦你掌握了函數式,而後你再學習響應式編程那就比較容易懂了,這是我親身體會的。我以前在學 Rxjs 的時候是真的痛苦,說實話,Rxjs 是我學過最難的庫了,沒有之一。在經歷過痛苦的一兩個月以後,有些東西仍是不能融會貫通,知道我最近研究函數式編程,才以爲是理所固然。毫無誇張,我也儘可能在後面的文章中給你們介紹一下 Rxjs,這個話題我也在公司分享過。數組

什麼是函數式編程?

簡單來講,函數式編程是一種強調以函數使用爲主的軟件開發風格。看到這句我想你仍是一臉懵逼,不知道函數式編程是啥,不要着急,看到最後我相信你會明白的。瀏覽器

還有一點你要記住,函數式編程的目的是使用函數來抽象做用在數據之上的控制流和操做,從而在系統中消除反作用減小對狀態的改變。數據結構

下面咱們經過例子來簡單的演示一下函數式編程的魅力。app

如今的需求就是輸出在網頁上輸出 「Hello World」

可能初學者會這麼寫。

document.querySelector('#msg').innerHTML = '<h1>Hello World</h1>'
複製代碼

這個程序很簡單,可是全部代碼都是死的,不能重用,若是想改變消息的格式、內容等就須要重寫整個表達式,因此可能有經驗的前端開發者會這麼寫。

function printMessage(elementId, format, message) {
    document.querySelector(elementId).innerHTML = `<${format}>${message}</${format}>`
}

printMessage('msg', 'h1', 'Hello World')
複製代碼

這樣確實有所改進,可是仍然不是一段可重用的代碼,若是是要將文本寫入文件,不是非 HTML,或者我想重複的顯示 Hello World

那麼做爲一個函數式開發者會怎麼寫這段代碼呢?

const printMessage = compose(addToDom('msg'), h1, echo)

printMessage('Hello World')
複製代碼

解釋一下這段代碼,其中的 h1echo 都是函數,addToDom 很明顯也能看出它是函數,那麼咱們爲何要寫成這樣呢?看起來多了不少函數同樣。

其實咱們是講程序分解爲一些更可重用、更可靠且更易於理解的部分,而後再將他們組合起來,造成一個更易推理的程序總體,這是咱們前面談到的基本原則。

compose 簡單解釋一下,他會讓函數從最後一個參數順序執行到第一個參數,compose 的每一個參數都是函數,不明白的能夠查一下,在 redux 的中間件部分這個是精華。

能夠看到咱們是將一個任務拆分紅多個最小顆粒的函數,而後經過組合的方式來完成咱們的任務,這跟咱們組件化的思想很相似,將整個頁面拆分紅若干個組件,而後拼裝起來完成咱們的整個頁面。在函數式編程裏面,組合是一個很是很是很是重要的思想。

好,咱們如今再改變一下需求,如今咱們須要將文本重複三遍,打印到控制檯。

var printMessaage = compose(console.log, repeat(3), echo)

printMessage(‘Hello World’)
複製代碼

能夠看到咱們更改了需求並無去修改內部邏輯,只是重組了一下函數而已。

能夠看到函數式編程在開發中具備聲明模式。爲了充分理解函數式編程,咱們先來看下幾個基本概念。

  • 聲明式編程
  • 純函數
  • 引用透明
  • 不可變性

聲明式編程

函數式編程屬於聲明是編程範式:這種範式會描述一系列的操做,但並不會暴露它們是如何實現的或是數據流如何傳過它們

咱們所熟知的 SQL 語句就是一種很典型的聲明式編程,它由一個個描述查詢結果應該是什麼樣的斷言組成,對數據檢索的內部機制進行了抽象

咱們再來看一組代碼再來對比一下命令式編程和聲明式編程。

// 命令式方式
var array = [0, 1, 2, 3]
for(let i = 0; i < array.length; i++) {
    array[i] = Math.pow(array[i], 2)
}

array; // [0, 1, 4, 9]

// 聲明式方式
[0, 1, 2, 3].map(num => Math.pow(num, 2))
複製代碼

能夠看到命令式很具體的告訴計算機如何執行某個任務。

而聲明式是將程序的描述與求值分離開來。它關注如何用各類表達式來描述程序邏輯,而不必定要指明其控制流或狀態關係的變化。

爲何咱們要去掉代碼循環呢?循環是一種重要的命令控制結構,但很難重用,而且很難插入其餘操做中。而函數式編程旨在儘量的提升代碼的無狀態性和不變性。要作到這一點,就要學會使用無反作用的函數--也稱純函數

純函數

純函數指沒有反作用的函數。相同的輸入有相同的輸出,就跟咱們上學的函數同樣。

經常這些狀況會產生反作用。

  • 改變一個全局的變量、屬性或數據結構
  • 改變一個函數參數的原始值
  • 處理用戶輸入
  • 拋出一個異常
  • 屏幕打印或記錄日誌
  • 查詢 HTML 文檔,瀏覽器的 Cookie 或訪問數據庫

舉一個簡單的例子

var counter = 0
function increment() {
    return ++counter;
}
複製代碼

這個函數就是不純的,它讀取了外部的變量,可能會以爲這段代碼沒有什麼問題,可是咱們要知道這種依賴外部變量來進行的計算,計算結果很難預測,你也有可能在其餘地方修改了 counter 的值,致使你 increment 出來的值不是你預期的。

對於純函數有如下性質:

  • 僅取決於提供的輸入,而不依賴於任何在函數求值或調用間隔時可能變化的隱藏狀態和外部狀態。
  • 不會形成超出做用域的變化,例如修改全局變量或引用傳遞的參數。

可是在咱們平時的開發中,有一些反作用是難以免的,與外部的存儲系統或 DOM 交互等,可是咱們能夠經過將其從主邏輯中分離出來,使他們易於管理。

如今咱們有一個小需求:經過 id 找到學生的記錄並渲染在瀏覽器(在寫程序的時候要想到可能也會寫到控制檯,數據庫或者文件,因此要想如何讓本身的代碼能重用)中。

// 命令式代碼

function showStudent(id) {
    // 這裏假如是同步查詢
    var student = db.get(id)
    if(student !== null) {
          // 讀取外部的 elementId
          document.querySelector(`${elementId}`).innerHTML = `${student.id},${student.name},${student.lastname}`
    } else {
        throw new Error('not found')
    }
}

showStudent('666')

// 函數式代碼

// 經過 find 函數找到學生
var find = curry(function(db, id) {
    var obj = db.get(id)
    if(obj === null) {
        throw new Error('not fount')
    }
    
    return obj
})

// 將學生對象 format
var csv = (student) => `${student.id},${student.name},${student.lastname}`

// 在屏幕上顯示
var append = curry(function(elementId, info) {
    document.querySelector(elementId).innerHTML = info
})

var showStudent = compose(append('#student-info'), csv, find(db))

showStudent('666')
複製代碼

若是看不懂 curry (柯里化)的先不着急,這是一個對於新手來講比較難理解的一個概念,在函數式編程裏面起着相當重要的做用。

能夠看到函數式代碼經過較少這些函數的長度,將 showStudent 編寫爲小函數的組合。這個程序還不夠完美,可是已經能夠展示出相比於命令式的不少優點了。

  • 靈活。有三個可重用的組件
  • 聲明式的風格,給高階步驟提供了一個清晰視圖,加強了代碼的可讀性
  • 另外是將純函數與不純的行爲分離出來。

咱們看到純函數的輸出結果是一致的,可預測的,相同的輸入會有相同的返回值,這個其實也被稱爲引用透明

引用透明

引用透明是定義一個純函數較爲正確的方法。純度在這個意義上表示一個函數的參數和返回值之間映射的純的關係。若是一個函數對於相同的輸入始終產生相同的結果,那麼咱們就說它是引用透明

這個概念很容易理解,簡單的舉兩個例子就好了。

// 非引用透明
var counter = 0

function increment() {
    return ++counter
}

// 引用透明
var increment = (counter) => counter + 1
複製代碼

其實對於箭頭函數在函數式編程裏面有一個高大上的名字,叫 lambda 表達式,對於這種匿名函數在學術上就是叫 lambda 表達式,如今在 Java 裏面也是支持的。

不可變數據

不可變數據是指那些建立後不能更改的數據。與許多其餘語言同樣,JavaScript 裏有一些基本類型(String,Number 等)從本質上是不可變的,可是對象就是在任意的地方可變。

考慮一個簡單的數組排序代碼:

var sortDesc = function(arr) {
    return arr.sort(function(a, b) {
        return a - b
    })
}

var arr = [1, 3, 2]
sortDesc(arr) // [1, 2, 3]
arr // [1, 2, 3]
複製代碼

這段代碼看似沒什麼問題,可是會致使在排序的過程當中會產生反作用,修改了原始引用,能夠看到原始的 arr 變成了 [1, 2, 3]。這是一個語言缺陷,後面會介紹如何克服。

總結

  • 使用純函數的代碼毫不會更改或破壞全局狀態,有助於提升代碼的可測試性和可維護性
  • 函數式編程採用聲明式的風格,易於推理,提升代碼的可讀性。
  • 函數式編程將函數視爲積木,經過一等高階函數來提升代碼的模塊化和可重用性。
  • 能夠利用響應式編程組合各個函數來下降事件驅動程序的複雜性(這點後面可能會單獨拿一篇來進行講解)。

內容來至於《JavaScript函數式編程指南》

歡迎關注我的公衆號【前端桃園】,公號更新頻率比掘金快。

相關文章
相關標籤/搜索