學會使用函數式編程的程序員(第3部分)

圖片描述

想閱讀更多優質文章請猛戳GitHub博客,一年百來篇優質文章等着你!javascript

本系列的其它篇:前端

  1. 學會使用函數式編程的程序員(第1部分)
  2. 學會使用函數式編程的程序員(第2部分)

引用透明 (Referential Transparency)

圖片描述

引用透明是一個富有想象力的優秀術語,它是用來描述純函數能夠被它的表達式安全的替換,經過下例來幫助咱們理解。java

在代數中,有一個以下的公式:git

y = x + 10

接着:程序員

x = 3

而後帶入表達式:github

y = 3 + 10

注意這個方程仍然是有效的,咱們能夠利用純函數作一些相同類型的替換。編程

下面是一個 JavaScript 的方法,在傳入的字符串兩邊加上單引號:segmentfault

function quote (str) {
  retrun "'" + str + "'"
}

下面是調用它:數組

function findError (key) {
     return "不能找到 " + quote(key)
   }

當查詢 key 值失敗時,findError 返回一個報錯信息。瀏覽器

由於 quote 是純函數,咱們能夠簡單地將 quote 函數體(這裏僅僅只是個表達式)替換掉在findError中的方法調用:

function findError (key) {
     return "不能找到 " + "'" + str + "'"
   }

這個就是一般所說的「反向重構」(它對我而言有更多的意義),能夠用來幫程序員或者程序(例如編譯器和測試程序)推理代碼的過程一個很好的方法。如,這在推導遞歸函數時尤爲有用的。

執行順序 (Execution Order)

圖片描述

大多數程序都是單線程的,即一次只執行一段代碼。即便你有一個多線程程序,大多數線程都被阻塞等待I/O完成,例如文件,網絡等等。

這也是當咱們編寫代碼的時候,咱們很天然考慮按次序來編寫代碼:

1. 拿到麪包 
2. 把2片面包放入烤麪包機 
3. 選擇加熱時間 
4. 按下開始按鈕 
5. 等待麪包片彈出 
6. 取出烤麪包 
7. 拿黃油 
8. 拿黃油刀 
9. 製做黃油麪包

在這個例子中,有兩個獨立的操做:拿黃油以及 加熱麪包。它們在 步驟9 時開始變得相互依賴。

咱們能夠將 步驟7步驟8步驟1步驟6 同時執行,由於它們彼此獨立。當咱們開始作的時候,事情開始複雜了:

線程一
--------------------------
1. 拿到麪包 
2. 把2片面包放入烤麪包機 
3. 選擇加熱時間 
4. 按下開始按鈕 
5. 等待麪包片彈出 
6. 取出烤麪包 

線程二
-------------------------
1. 拿黃油 
2. 拿黃油刀 
3. 等待線程1完成 
4. 取出烤麪包

果線程1失敗,線程2怎麼辦? 怎麼協調這兩個線程? 烤麪包這一步驟在哪一個線程運行:線程1,線程2或者二者?

不考慮這些複雜性,讓咱們的程序保持單線程會更容易。可是,只要可以提高咱們程序的效率,要付出努力來寫好多線程程序,這是值得的。

然而,多線程有兩個主要問題:

  1. 多線程程序難於編寫、讀取、解釋、測試和調試。
  2. 一些語言,例如JavaScript,並不支持多線程,就算有些語言支持多線程,對它的支持也很弱。

可是,若是順序可有可無,全部事情都是並行執行的呢?

儘管這聽起來有些瘋狂,但其實並不像聽起來那麼混亂。讓咱們來看一下 Elm 的代碼來形象的理解它:

buildMessage message value =
    let
        upperMessage =
            String.toUpper message
        quotedValue =
            "'" ++ value ++ "'"
    in
        upperMessage ++ ": " ++ quotedValue

這裏的 buildMessage 接受參數 messagevalue,而後,生成大寫的 message和 帶有引號的 value

注意到 upperMessagequotedValue 是獨立的。咱們怎麼知道的呢?

在上面的代碼示例中,upperMessagequotedValue 二者都是純的而且沒有一個須要依賴其它的輸出。

若是它們不純,咱們就永遠不知道它們是獨立的。在這種狀況下,咱們必須依賴程序中調用它們的順序來肯定它們的執行順序。這就是全部命令式語言的工做方式。

第二點必須知足的就是一個函數的輸出值不能做爲其它函數的輸入值。若是存在這種狀況,那麼咱們不得不等待其中一個完成才能執行下一個。

在本例中,upperMessagequotedValue 都是純的而且沒有一個須要依賴其它的輸出,所以,這兩個函數能夠以任何順序執行。

編譯器能夠在不須要程序員幫助的狀況下作出這個決定。這隻有在純函數式語言中才有可能,由於很難(若是不是不可能的話)肯定反作用的後果。

在純函數語言中,執行的順序能夠由編譯器決定。

考慮到 CPU 沒法一再的加快速度,這種作法很是有利的。別一方面,生產商也不斷增長CPU內核芯片的數量,這意味着代碼能夠在硬件層面上並行執行。使用純函數語言,就有但願在不改變任何代碼的狀況下充分地發揮 CPU 芯片的功能並取得良好成效。

類型註釋 (Type Annotations)

圖片描述

在靜態類型語言中,類型是內聯定義的。如下是 Java 代碼:

public static String quote(String str) {
    return "'" + str + "'";
}

注意類型是如何同函數定義內聯在一塊兒的。當有泛型時,它變的更糟:

private final Map<Integer, String> getPerson(Map<String, String> people, Integer personId) {
   // ...
}

這裏使用粗體標出了使它們使用的類型,但它們仍然會讓函數可讀性下降,你必須仔細閱讀才能找到變量的名稱。

對於動態類型語言,這不是問題。在 Javascript 中,能夠編寫以下代碼:

var getPerson = function(people, personId) {
    // ...
};

這樣沒有任何的的類型信息更易於閱讀,惟一的問題就是放棄了類型檢測的安全特性。這樣可以很簡單的傳入這些參數,例如,一個 Number 類型的 people 以及一個 Objec t類型的 personId

動態類型要等到程序執行後才能知道哪裏問題,這多是在發佈的幾個月後。在 Java 中不會出現這種狀況,由於它不能被編譯。

可是,假如咱們能同時擁有這二者的優異點呢? JavaScript 的語法簡單性以及 Java 的安全性。

事實證實咱們能夠。下面是 Elm 中的一個帶有類型註釋的函數:

add : Int -> Int -> Int
add x y =
    x + y

請注意類型信息是在單獨的代碼行上面的,而正是這樣的分割使得其有所不一樣。

如今你可能認爲類型註釋有錯訓。 第一次見到它的時候。 大都認爲第一個 -> 應該是一個逗號。能夠加上隱含的括號,代碼就清晰多了:

add : Int -> (Int -> Int)

上例 add 是一個函數,它接受類型爲 Int 的單個參數,並返回一個函數,該函數接受單個參數 Int類型 並返回一個 Int 類型的結果。

如下是一個帶括號類型註釋的代碼:

doSomething : String -> (Int -> (String -> String)) 
doSomething prefix value suffix = 
prefix ++ (toString value) ++ suffix

這裏 doSomething 是一個函數,它接受 String 類型的單個參數,而後返回一個函數,該函數接受 Int 類型的單個參數,而後返回一個函數,該函數接受 String 類型的單個參數,並返回一個字符串。

注意爲何每一個方法都只接受一個參數呢? 這是由於每一個方法在 Elm 裏面都是柯里化。

由於括號老是指向右邊,它們是沒必要要的,簡寫以下:

doSomething : String -> Int -> String -> String

當咱們將函數做爲參數傳遞時,括號是必要的。若是沒有它們,類型註釋將是不明確的。例如:

takes2Params : Int -> Int -> String
takes2Params num1 num2 =
    -- do something

很是不一樣於:

takes1Param : (Int -> Int) -> String
takes1Param f =
    -- do something

takes2Param 函數須要兩個參數,一個 Int 和另外一個 Int,而takes1Param 函數須要一個參數,這個參數爲函數, 這個函數須要接受兩個 Int 類型參數。

下面是 map 的類型註釋:

map : (a -> b) -> List a -> List b
map f list =
    // ...

這裏須要括號,由於 f 的類型是(a -> b),也就是說,函數接受類型 a 的單個參數並返回類型 b 的某個函數。

這裏類型 a 是任何類型。當類型爲大寫形式時,它是顯式類型,例如 String。當一個類型是小寫時,它能夠是任何類型。這裏 a 能夠是字符串,也能夠是 Int

若是你看到 (a -> a) 那就是說輸入類型和輸出類型必須是相同的。它們是什麼並不重要,但必須匹配。

但在 map 這一示例中,有這樣一段 (a -> b)。這意味着它既能返回一個不一樣的類型,也能返回一個相同的類型。

可是一旦 a 的類型肯定了,a 在整段代碼中就必須爲這個類型。例如,若是 a 是一個 Int,b 是一個 String,那麼這段代碼就至關於:

(Int -> String) -> List Int -> List String

這裏全部的 a 都換成了 Int,全部的 b 都換成了 String

List Int 類型意味着一個值都爲 Int 類型的列表, List String 意味着一個值都爲 String 類型的列表。若是你已經在 Java 或者其餘的語言中使用過泛型,那麼這個概念你應該是熟悉的

函數式 JavaScript

圖片描述

JavaScript 擁有不少類函數式的特性但它沒有純性,可是咱們能夠設法獲得一些不變量和純函數,甚至能夠藉助一些庫。

但這並非理想的解決方法。若是你不得不使用純特性,爲什麼不直接考慮函數式語言?

這並不理想,但若是你必須使用它,爲何不從函數式語言中得到一些好處呢?

不可變性(Immutability)

首先要考慮的是不變性。在ES2015或ES6中,有一個新的關鍵詞叫const,這意味着一旦一個變量被設置,它就不能被重置:

const a = 1;
a = 2; // 這將在Chrome、Firefox或 Node中拋出一個類型錯誤,但在Safari中則不會

在這裏,a 被定義爲一個常量,所以一旦設置就不能更改。這就是爲何 a = 2 拋出異常。

const 的缺陷在於它不夠嚴格,咱們來看個例子:

const a = {
    x: 1,
    y: 2
};
a.x = 2; // 沒有異常
a = {}; // 報錯

注意到 a.x = 2 沒有拋出異常。const 關鍵字惟一不變的是變量 a, a 所指向的對象是可變的。

那麼Javascript中如何得到不變性呢?

不幸的是,咱們只能經過一個名爲 Immutable.js 的庫來實現。這可能會給咱們帶來更好的不變性,但遺憾的是,這種不變性使咱們的代碼看起來更像 Java 而不是 Javascript。

柯里化與組合 (curring and composition)

在本系列的前面,咱們學習瞭如何編寫柯里化函數,這裏有一個更復雜的例子:

const f = a => b => c => d => a + b + c + d

咱們得手寫上述柯里化的過程,以下:

console.log(f(1)(2)(3)(4)); // prints 10

括號如此之多,但這已經足夠讓Lisp程序員哭了。有許多庫能夠簡化這個過程,我最喜歡的是 Ramda

使用 Ramda 簡化以下:

const f = R.curry((a, b, c, d) => a + b + c + d);
console.log(f(1, 2, 3, 4)); // prints 10
console.log(f(1, 2)(3, 4)); // also prints 10
console.log(f(1)(2)(3, 4)); // also prints 10

函數的定義並無好多少,可是咱們已經消除了對那些括號的須要。注意,調用 f 時,能夠指定任意參數。

重寫一下以前的 mult5AfterAdd10 函數:

const add = R.curry((x, y) => x + y);
const mult5 = value => value * 5;
const mult5AfterAdd10 = R.compose(mult5, add(10));

事實上 Ramda 提供了不少輔助函數來作些簡單常見的運算,好比R.add以及R.multiply。以上代碼咱們還能夠簡化:

const mult5AfterAdd10 = R.compose(R.multiply(5), R.add(10));

Map, Filter and Reduce

Ramda 也有本身的 mapfilterreduce 版本。雖然這些函數存在於數組中。這幾個函數是在 Array.prototype 對象中的,而在 Ramda 中它們是柯里化的

const isOdd = R.flip(R.modulo)(2);
const onlyOdd = R.filter(isOdd);
const isEven = R.complement(isOdd);
const onlyEven = R.filter(isEven);
const numbers = [1, 2, 3, 4, 5, 6, 7, 8];
console.log(onlyEven(numbers)); // prints [2, 4, 6, 8]
console.log(onlyOdd(numbers)); // prints [1, 3, 5, 7]

R.modulo 接受2個參數,被除數和除數。

isOdd 函數表示一個數除 2 的餘數。若餘數爲 0,則返回 false,即不是奇數;若餘數爲 1,則返回 true,是奇數。用 R.filp 置換一下 R.modulo 函數兩個參數順序,使得 2 做爲除數。

isEven 函數是 isOdd 函數的補集。

onlyOdd 函數是由 isOdd 函數進行斷言的過濾函數。當它傳入最後一個參數,一個數組,它就會被執行。

同理,onlyEven 函數是由 isEven 函數進行斷言的過濾函數。

當咱們給函數 onlyEvenonlyOd 傳入 numbersisEvenisOdd 得到了最後的參數,而後執行最終返回咱們指望的數字。

Javascript的缺點

圖片描述

全部的庫和語言加強都已經獲得了Javascript 的發展,但它仍然面臨着這樣一個事實:它是一種強制性的語言,它試圖爲全部人提供全部的東西。

大多數前端開發人員都不得不使用 Javascript,由於這旨瀏覽器也識別的語言。相反,它們使用不一樣的語言編寫,而後編譯,或者更準確地說,是把其它語言轉換成 Javascript。

CoffeeScript 是這類語言中最先的一批。目前,TypeScript 已經被 Angular2 採用,Babel能夠將這類語言編譯成 JavaScript,愈來愈多的開發者在項目中採用這種方式。

可是這些語言都是從 Javascript 開始的,而且只稍微改進了一點。爲何不直接從純函數語言轉換到Javascript呢?

將來期盼

圖片描述

咱們不可能知道將來會怎樣,但咱們能夠作一些有根據的猜想。如下是做者的一些見解:

  1. 能轉換成 JavaScript 這類語言會有更加豐富及健壯。
  2. 已有40多年曆史的函數式編程思想將被從新發現,以解決咱們當前的軟件複雜性問題。
  3. 目前的硬件,好比廉價的內存,快速的處理器,使得函數式技術普及成爲可能。
  4. PU不會變快,可是內核的數量會持續增長。
  5. 可變狀態將被認爲是複雜系統中最大的問題之一。

但願這系列文章能幫助你更好容易更好幫助你理解函數式編程及優點,做者相信函數式編程是將來趨勢,你們有時間能夠多多瞭解,接着提高大家的技能,而後將來有更好的出路。

原文:

  1. https://medium.com/@cscalfani...
  2. https://medium.com/@cscalfani...

編輯中可能存在的bug無法實時知道,過後爲了解決這些bug,花了大量的時間進行log 調試,這邊順便給你們推薦一個好用的BUG監控工具Fundebug

你的點贊是我持續分享好東西的動力,歡迎點贊!

歡迎加入前端你們庭,裏面會常常分享一些技術資源。

clipboard.png

相關文章
相關標籤/搜索