JavaScript 函數式編程(一)

零、前言

說到函數式編程,想必各位或多或少都有所耳聞,然而對於函數式的內涵和本質可能又有些說不清楚。javascript

因此本文但願針對工程師,從應用(而非學術)的角度將函數式編程相關思想和實踐(以 JavaScript 爲例)分享給你們。html

文章內容其實主要來自於在下閱讀各種參考文獻後的再整理,因此有什麼錯誤也但願你們幫忙斧正~java

slide 地址git

1、什麼是函數式編程?

Functional programming is a programming paradigm程序員

1.treats computation as the evaluation of mathematical functionses6

2.avoids changing-state and mutable datagithub

by wikipedia數據庫

從以上維基百科的定義來看有三個要點編程

  • Programming Paradigm:編程範式
  • Mathematical Functions:數學函數
  • Changing-state And Mutable Data:改變狀態和可變數據

下面分別解析一下以上要點。數組

1.1.什麼是編程範式?

programming paradigm

from Programming paradigms

編程範式從概念上來說指的是編程的基本風格和典範模式。

換句話說其實就是程序員對於如何使用編程來解決問題的世界觀和方法論

若是把一門編程語言比做兵器,它的語法、工具和技巧等是招法,那麼它採用的編程範式也就是是內功心法。

一種範式能夠在不一樣的語言中實現,一種語言也能夠同時支持多種範式。例如 JavaScript 就是一種多範式的語言。

1.2.什麼是數學函數?

通常的,在一個變化過程當中,假設有兩個變量 x、y,若是對於任意一個 x 都有惟一肯定的一個y和它對應,那麼就稱 x 是自變量,y 是 x 的函數。x 的取值範圍叫作這個函數的定義域,相應 y 的取值範圍叫作函數的值域。

以上定義,在初中數學我們都應該學過...

換句話說,函數只是兩種數值之間的關係:輸入和輸出。

儘管每一個輸入都只會有一個輸出,但不一樣的輸入卻能夠有相同的輸出。下圖展現了一個合法的從 x 到 y 的函數關係;

純函數

與之相反,下面這張圖表展現的就不是一種函數關係,由於輸入值 5 指向了多個輸出:

非純函數

1.2.1.什麼是純函數(Pure Functions)?

純函數是這樣一種函數,對於相同的輸入,永遠會獲得相同的輸出,並且沒有任何可觀察的反作用。

根據定義能夠看出純函數其實就是數學函數,即表示從輸入的參數到輸出結果的映射。

而沒有反作用的純函數顯然都是引用透明的。

引用透明性(Referential Transparency)指的是,若是一段代碼在不改變整個程序行爲的前提下,能夠替換成它的執行結果。

const double = x => x * 2
const addFive = x => x + 5
const num = double(addFive(10))

num === double(10 + 5)
    === double(15)
    === 15 * 2
    === 30
複製代碼

不過說了半天,反作用又是啥...?

1.2.2.什麼是反作用(Side Effects)?

反作用是在計算的過程當中,系統狀態的一種變化,或者與外部世界進行的可觀察的交互。

反作用可能包含,但不限於如下行爲:

  • 更改文件系統
  • 往數據庫中插入記錄
  • 發送一個 http 請求
  • 改變數據
  • 打印 log
  • 獲取用戶輸入
  • DOM 查詢
  • 訪問系統狀態
  • ...

只要是跟函數外部環境發生的交互就都是反作用——這一點可能會讓你懷疑無反作用編程的可行性。

函數式編程的哲學就是假定反作用是形成不正當行爲的主要緣由。

固然這並非說,要禁止使用一切反作用,而是說,要讓它們在可控的範圍內發生。

在後面講到函子(functor)和單子(monad)的時候咱們會學習如何控制它們。

1.2.3.純函數的好處都有啥~~(誰說對了就給他)~~?

面嚮對象語言的問題是,它們永遠都要隨身攜帶那些隱式的環境。你只須要一個香蕉,但卻獲得一個拿着香蕉的大猩猩...以及整個叢林

by Erlang 做者:Joe Armstrong

因此使用純函數將會有如下好處:

  • 可緩存性(Cacheable)
  • 可移植性/自文檔化(Portable / Self-Documenting)
  • 可測試性(Testable)
  • 合理性(Reasonable)
  • 並行代碼(Parallel Code)

1.3.爲何要避免改變狀態和可變數據?

Shared mutable state is the root of all evil

共享可變狀態是萬惡之源

by Pete Hunt

the-root-of-all-evil

const obj = { val: 1 }
someFn(obj)
console.log(obj) // ???
複製代碼

shared-mutable-state

from Building Scalable, Highly Concurrent & Fault Tolerant Systems - Lessons Learned

1.4.原教旨函數式 VS 溫和派函數式?

說到函數式編程語言,你們的第一反應多是 Haskell、OCaml、Lisp、Erlang、Scala、F#...

由於它們可能有如下特性:

  • 函數是「一等公民」(first class)
  • 不可變數據
  • 使用遞歸而不是循環
  • 柯里化
  • 惰性求值
  • 代數數據類型
  • 模式匹配
  • ...

javascript_functional_lite

而說到 JavaScript,不少人可能第一反應認爲這是一門面向對象的語言。

可是想一想前面說的:函數式編程只是一種編程範式,而編程範式就像「內功心法」,因此與以上這些語言特性不徹底相關,反而與你本身的編程思惟(即世界觀和方法論)更加相關。

在函數式方面,因爲 JavaScript 支持高階函數、匿名函數、函數是一等公民、閉包、解構(模式匹配)等特性,因此它也能支持函數式編程範式。(雖然不是那麼的原教旨函數式,但還基本夠用~尤爲是 ES6 新增的箭頭函數等特性~還有各類類庫 )

事實上 JavaScript 是一門基於原型(prototype-based)的多範式語言。

1.5.做爲函數式語言 JavaScript 還差什麼?

1.5.1.不可變數據結構

JavaScript 一共有 6 種原始類型(包括 ES6 新添加的 Symbol 類型),它們分別是 Boolean,Null,Undefined,Number,String 和 Symbol。 除了這些原始類型,其餘的類型都是 Object,而 Object 都是可變的。

data-type-in-js

1.5.2.惰性求值

惰性(lazy)指求值的過程並不會馬上發生。

好比一些數學題,咱們可能一開始並不須要把全部表達式都求值,這樣能夠在計算的過程當中將一些表達式消掉。

惰性求值是相對於**及早求值(eager evaluation)**的。

好比大部分語言中,參數中的表達式都會被先求值,這也稱爲應用序語言。

好比看下面這樣一個 JavaScript 的函數:

wholeNameOf(getFirstName(), getLastName())
複製代碼

getFirstNamegetLastName 會依次執行,返回值做爲 wholeNameOf 函數的參數, wholeNameOf 函數最後才被調用。

另外,對於數組操做時,大部分語言也一樣採用的是應用序。

[1, 2, 3, 4].map(x => x + 1)
複製代碼

因此,這個表達式馬上會返回結果 [2, 3, 4, 5] 。

固然這並非說 JavaScript 語言使用應用序有問題,可是沒有提供惰性序列的支持就是 JavaScript 的不對了。若是 map 一個大數組後咱們發現其實只須要前 10 個元素時,去計算全部元素就顯得多餘了。

1.5.3.函數組合

面向對象一般被比喻爲名詞,而函數式編程是動詞。面向對象抽象的是對象,對於對象的的描述天然是名詞。

面向對象把全部操做和數據都封裝在對象內,經過接受消息作相應的操做。好比,對象 Kitty,它們能夠接受「打招呼」的消息,而後作相應的動做。

而函數式的抽象方式恰好相反,是把動做抽象出來,好比「打招呼」就是一個函數,而函數參數就是做爲數據傳入的 Kitty(即 Kitty 進入函數「打招呼」,出來的應該是 Hello Kitty)。

面向對象能夠經過繼承和組合在對象之間分享一些行爲或者說屬性,函數式的思路就是經過組合已有的函數造成一個新的函數。

然而 JavaScript 語言雖然支持高階函數,可是並無一個原生的利於組合函數產生新函數的方式。而這些強大的函數組合方式卻每每被相似 Underscore,Lodash 等工具庫的光芒掩蓋掉(後面會說到這些庫的問題)。

1.5.4.尾遞歸優化

tail-calls

函數式編程語言中由於不可變數據結構的緣由,沒辦法實現循環。因此都是經過遞歸來實現循環。

然而遞歸使用不當很容易棧溢出(Stack Overflow),因此通常採用尾遞歸的方式來優化。

雖然 ES6 規範中規定了尾遞歸優化規範,然而提供實現的解釋器還很是的少,詳情能夠查閱這個連接

1.5.5.代數類型系統

JavaScript 做爲一種弱類型的語言,沒有靜態類型系統。不過使用一些 TypeScript 等預編譯的語言能夠做爲補充~

2、聲明式 VS 命令式

Declarative VS Imperative,這二者的區別簡單來講其實就是 What VS How。

2.1.「意識形態」上的區別~

聲明式:

  • 程序抽象了控制流過程,代碼描述的是 —— 數據流:即作什麼。
  • 更多依賴表達式。

表達式是指一小段代碼,它用來計算某個值。表達式一般是某些函數調用的複合、一些值和操做符,用來計算出結果值。

命令式:

  • 代碼描述用來達成指望結果的特定步驟 —— 控制流:即如何作。
  • 頻繁使用語句。

語句是指一小段代碼,它用來完成某個行爲。通用的語句例子包括 for、if、switch、throw,等等……

2.2.舉一些栗子🌰...

例1:但願獲得一個數組每一個數據平方後的和

// 命令式
function mysteryFn (nums) {
  let squares = []
  let sum = 0                           // 1. 建立中間變量

  for (let i = 0; i < nums.length; i++) {
    squares.push(nums[i] * nums[i])     // 2. 循環計算平方
  }

  for (let i = 0; i < squares.length; i++) {
    sum += squares[i]                   // 3. 循環累加
  }

  return sum
}

// 以上代碼都是 how 而不是 what...

// 函數式
const mysteryFn = (nums) => nums
  .map(x => x * x)                      // a. 平方
  .reduce((acc, cur) => acc + cur, 0)   // b. 累加
複製代碼

例2:但願獲得一個數組全部偶數值的一半的平均值

// 命令式
function mysteryFn(nums) {
  let sum = 0
  let tally = 0                         // 1. 建立中間變量

  for (let i = 0; i < nums.length; i++) {
    if (nums[i] % 2 === 0) {            // 2. 循環,值爲偶數時累加該值的一半並記錄數量
      sum += nums[i] / 2
      tally++
    }
  }

  return tally === 0 ? 0 : sum / tally  // 3. 返回平均值
}

// 函數式
const mysteryFn = (nums) => nums
  .filter(x => x % 2 === 0)             // a. 過濾非偶數
  .map(x => x / 2)                      // b. 折半
  .reduce((acc, cur, idx, { length }) => (
    idx < length - 1
      ? acc + cur                       // c. 累加
      : (acc + cur) / length            // d. 計算平均值
  ), 0)
複製代碼

參考資料

相關文章

以上 to be continued...

相關文章
相關標籤/搜索