函數式編程中的 “函數們”

FernandoOPA 7年前發布 | 19K 次閱讀 函數式編程 JavaScript

函數式編程中函數有三種不同的解讀方式,分別為純函數、高階函數和一等函數。本文分別對這三者的概念、應用和聯系進行詳解。

純函數

定義:

1. 相同的輸入必定產生相同的輸出

2. 在計算的過程中,不會產生副作用

滿足上述兩個條件,我們就說該函數是純函數。

純函數也即數學意義上的函數,表達的是數據之間的轉換(映射)關系,而非計算步驟的詳述。數學函數的定義:

函數通常由定義域 X 、值域 Y, 以及定義域到值域的映射 ff: X -> Y )組成。

純函數讓我們對寫出的函數具有完全的控制能力。純函數的結果 必須 只依賴于輸入參數,不受外部環境的影響;同時純函數在計算結果的過程中,也不會影響(污染)外部環境,即不會產生副作用。

函數組合

純函數定義中的兩個條件保證了它(的計算過程)與外界是完全隔離,這也是函數組合的基礎。

只有函數組合中的所有函數都是純函數,我們組合起來的新函數才會是純函數。我們可以對使用純函數組合出來的新函數從數學上證明(推導)其正確性,而無需借助大量的單元測試。

只要在函數組合時引入一個非純函數,整個組合出來的函數將淪為非純函數。如果將函數組合比作管道的拼接,只要組成管道的任何一小節有泄露或者外部注入,我們便失去了對整條管道的完全控制。

要想實現函數組合,還需要滿足連續性,描述如下:

因為純函數可以看作定義域到值域映射,待組合的函數中,上一個函數的值域須等于下一個函數的定義域,也即上一個函數的輸出(類型)等于下一個的輸入(類型)。

假設有兩個函數: f: X -> Yg: Y -> Z ,只有 codomain(f) = domain(g) 時, fg 才可以組合。

引用透明及緩存

在不改變整個程序行為的情況下,如果能將其中的一段代碼替換為其執行的結果,我們就說這段代碼是引用透明的。

因此,執行一段引用透明的代碼(函數),對于相同的參數,總是給出相同的結果。我們也稱這樣的函數(代碼)為純函數。

引用透明的一個典型應用即函數緩存。我們可以將已經執行過的函數輸入值緩存起來,下次調用時,若輸入值相同,直接跳過計算過程,用緩存結果代替計算結果返回即可。

函數緩存的實現依賴于閉包,而閉包的實現又依賴于高階函數,高階函數的實現又依賴于一等函數。我們按照這條依賴鏈,從里往外依次對它們進行講解。

一等函數(First Class Functions)

程序語言會對基本元素的使用方式進行限制,帶有最少限制的元素被稱為一等公民,其擁有的 “權利” 如下:

  • 可以使用變量命名;
  • 可以提供給函數作為參數;
  • 可以由函數作為結果返回;
  • 可以包含在數據結構中;

乍一看,我們應該首先會想到程序中的基本數據結構(如 number、array、object 等)是一等公民。如果函數也被視為一等公民,我們便可以像使用普通數據一樣對其使用變量命名,作為參數或返回值使用,或者將其包含在數據結構中。在這里函數和數據的邊界開始變得不再那么分明了。函數被視為一等公民后,其能力和適用范圍被大大擴展了。

下面使用 JavaScript 對上面第一條和第四條 “權利” 進行講解。第二、三條與高階函數密切相關,將放到下一節的高階函數中講解。

使用變量命名

const square = x => x * x

上面代碼定義了一個求平方值的函數,并將其賦給了 square 變量。

可以包含在數據結構中

Ramda 中有一個API:evolve ,其接受的首個參數便是一個屬性值為函數的對象。evolve 函數會遞歸地對 “待處理對象” 的屬性進行變換,變換的方式由 transformation 內置函數屬性值的對象定義。示例如下(示例中的 R.xxx 都是 Ramda 中的API,相關API的功能可以參考Ramda 文檔):

var tomato  = {name: 'Tomato', data: {elapsed: 100, remaining: 1400}, id:123};
var transformations = {
  name: R.toUpper,
  data: {elapsed: R.add(1), remaining: R.add(-1)}
};

R.evolve(transformations)(tomato);
//=> {name: 'TOMATO', data: {elapsed: 101, remaining: 1399}, id:123}

高階函數

定義:

使用函數作為輸入參數,或者返回結果為函數的函數,被稱為高階函數。

作為參數或返回值的函數,是一等函數的應用之一。高階函數以一等函數作為基礎,只有支持一等函數的語言才能進行高階函數編程。

以熟悉的 filter 函數為例,我們可以用 filter 對列表中的元素進行過濾,篩選出符合條件的元素。filter 的類型簽名和示例代碼如下:

filter :: (a → Boolean) → [a] → [a]
const isEven = n => n % 2 === 0;

const filterEven = R.filter(isEven);

filterEven([1, 2, 3, 4]); //=> [2, 4]

filter 接受一個判斷函數(判斷輸入值是否為偶數) isEven ,返回一個過濾出偶數的函數 filterEven 。

閉包

定義:

閉包是由函數及該函數捕獲的其上下文中的自由變量組成的記錄

舉例講:

function add(x) {
  const xIn = x;
  return function addInner(y) {
    return xIn + y;
  }
}
const inc = add(1);
inc(8); //=> 9;

const plus2 = add(2);
plus2(8); //=> 10;

上述代碼中返回的函數 addInner 及由其捕獲的在其上下文中定義的自由變量 xIn ,便組成了一個閉包。

上述代碼中最外層的 add 函數是一個高階函數,其返回值為一等函數 addInner 。

其實 add 函數的參數 x 也是 addInner 上下文的一部分,所以 ‘xIn’ 也就沒有存在的必要了, add 代碼優化如下:

function add(x) {
  return function addInner(y) {
    return x + y;
  }
}

借助于箭頭函數,我們可以進一步優化 add 的實現:

const add = x => y => x + y

是不是非常簡潔?由此我們可以一窺函數式編程強大的表達能力。

閉包主要用來做數據緩存,而數據緩存應用非常廣泛:包括函數工廠模式、模擬擁有私有變量的對象、函數緩存、還有大名鼎鼎的柯里化。

其實上述代碼中 add 函數便是柯里化形式的函數。

上述代碼中的 const inc = add(1); 和 const plus2 = add(2); 是一種函數工廠模式,通過向 add 函數傳入不同的參數,便會產生功能不同的函數。函數工廠可以提高函數的抽象和復用能力。

例如我們有一個如下形式的 Ajax 請求函數:

const ajax = method => type => query => { ... };

const get = ajax('GET');
const post = ajax('POST');

const getJson = get('json');
const getHtml = ajax('GET')('text/html') = get('text/html');

我們抽象出了最一般的 ajax 請求函數,在具體應用時,我們用能通過函數工廠生產出作用不同的函數。

通過上面幾個小節,我們講解純函數(數學意義上的函數)、一等函數、高階函數,還有閉包,下面通過集上述所有概念于一身的 函數緩存 ,來結束函數式編程中的 ”函數們“ 的論述。

函數緩存 memoize

函數實現:

const memoize = pureFunc => {
  const cache = {};
  return function() {
    const argStr = JSON.stringify(arguments);
    cache[argStr] = cache[argStr] || pureFunc.apply(pureFunc, arguments);
    return cache[argStr];
  };
};

memoize 的功能是對傳入函數 pureFunc 進行緩存,返回緩存版本的 pureFunc 。當我們使用參數調用緩存的函數時,緩存的函數會到 cache 中查找該參數是否被緩存過,如果有緩存,則不需要再次計算,直接返回已緩存值,否則對本次輸入的參數進行計算,緩存計算的結果以備后用,然后將結果返回。

memoize 只有對純函數的緩存才有意義。因為純函數是引用透明的,其輸出只依賴于輸入,并且計算過程不會影響外部環境。

舉一個極端的例子,假如我們有一個隨機數字生成函數 random() , 如果對其進行了緩存:

const memoizedRandom = memoize(random);

memoizedRandom 除了第一次生成一個隨機值外,隨后的調用都返回第一次緩存的值,這樣就失去了 random 的意義。再假如,我們對終端字符輸入函數 getchar() 進行了緩存,每次調用都會是第一次獲取的字母。

memoize 內部實現了一個閉包的創建。返回的緩存函數和自由變量 cache 共同構成了一個閉包。自由變量 cached 用于對已經計算過的數據(參數)的緩存。而閉包本身是由高階函數和一等函數實現的。

總結

本文對函數式編程中的 “函數們” 做了詳細解釋:純函數、一等函數、高階函數,并展示了它們的應用。其中純函數是函數組合的基礎;一等函數是高階函數的實現基礎,一等函數和高階函數又是閉包的實現基礎。

最后通過函數緩存函數 memoize 將純函數、一等函數、高階函數和閉包聯系起來,用函數式編程中的 “函數們” (函數式三鏢客)的一次 “聯合行動” 結束本文。

參考文檔

What is a Function? .

Functional Programming .

Referential Transparency .

 

來自:http://www.techug.com/post/functions-in-functional-programming.html

 

 本文由用戶 FernandoOPA 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!