1. 程式人生 > >JavaScript兩大支柱-PART2:函數語言程式設計

JavaScript兩大支柱-PART2:函數語言程式設計

JavaScript是有史以來最重要的程式語言之一,不僅僅是因為它的流行,而且因為它推廣了兩個對程式設計發展極為重要的特性:

原型繼承(沒有類的物件,原型委託,又名OLOO - 連結到其他物件的物件),和 函數語言程式設計(由帶閉包的lambdas啟用)

總的來說,我喜歡將這些範例稱為JavaScript的兩大支柱,我並不羞於承認它們已經影響了我, 我不想用沒有它們的語言程式設計。

我們今天正在構建未來

我滑到冰球將要在的地方,而不是它已經在的地方。~ Wayne Gretzky

高科技的世界建立在不斷創新的文化之上。 我們因為共同的需要而推動技術不斷超越前沿,從而一起建立了偉大的事物。每一項新的突破使我們能夠做我們幾年前從未想到過的事情。 歡迎來到技術的世界。

不僅事情變化很快,事情變化的速度也在迅速變化。 過去幾十年,許多科技迭代進入指數級的科技爆炸。 五年前我們構建典型應用程式的方式與我們今天構建應用程式的方式截然不同。 您現在可能正在使用2年前不存在的工具。 回到15年或20年,甚至我們的流程也完全不同(瀑布與敏捷等等)。

你花了兩年時間學習Angular嗎? 如果是這樣,我會感到痛苦。 您學到的很多東西都不適用於Angular 2.0或React。 如果我們繼續以我們一直以來的方式做事,這種變化的速度是不可持續的。

如果從長遠來看我們將作為工程師生存下去,我們必須學習的最重要的事情是如何適應,以及如何使我們的程式碼更具適應性。

我們必須快速學習,因為我們明年使用的技術可能比現在要複雜兩倍。 隨著物聯網和人工智慧繼續爆炸,我們應用程式的負載將繼續呈指數級增長。 明天的應用程式需要比現在更具可擴充套件性,可互操作性,併發性,連線性,高效能和智慧性。

我們能夠跟上指數級增加的複雜性的唯一方法是降低理解程式的複雜性。 為了維護明天的巨型應用程式,我們必須學會構建更具表現力的程式碼。 我們必須學會編寫更容易推理,除錯,建立,分析和理解的程式。

面向過程的程式設計和麵向物件的程式設計不會帶領我們到我們需要去的地方。

在接下來的幾年中,我們編碼的方式將以激進的方式發生變化,推動我們進入一個根本不同的方向,而不是過去30年我們一直在猛衝的方向。 這些變化將在程式設計技術,流程,應用程式可擴充套件性和質量控制方面帶來許多重要突破。

精通函數語言程式設計的開發人員在不久的將來會有很大的需求。

這種變化已經席捲了Netflix,Facebook和微軟等組織,並且程式設計風格的這種構造轉變是有非常充分的理由的。

在佛教哲學中,禪宗公案經常被用來挑戰學生對現實的基本理解,其中許多突出介紹了兩個看似矛盾的想法和意義。當學生意識到兩種觀點的真實性時,就可以說掌握了這個知識或者說得到了啟發。這是我的書《編寫JavaScript應用程式》中包含的現代版本:

尊敬的大師Qc Na和他的學生安東一起走路。 安東希望丟擲一個話題和大師討論,安東說:“師父,我聽說物件是一件非常好的東西 - 這是真的嗎?”Qc Na憐憫地看著他的學生並回答道,“愚蠢的學生 - 物件只是一個窮人的閉包“。 由於被嚴厲地批評,安東從他的主人那裡離開並回到他的房間,打算研究閉包。 他仔細閱讀了整個“Lambda:The Ultimate …”系列論文及其表兄弟,並實現了一個帶有基於閉包的物件系統的小型Scheme直譯器。 他學到了很多,並期待他的老師能看到他的進步。 在他與Qc Na的下一次行走時,安東試圖給師父留下好印象,說“師父,我已經努力研究這件事,並且現在明白物件真的是一個窮人的閉包。”Qc Na用他的棍子敲了安東,迴應道 “你什麼時候去學習? 閉包是一個窮人的物件。“那一刻,安東開悟了。

與物件一樣,閉包是一種包含狀態的機制。 在JavaScript中,只要函式訪問在立即函式作用域之外定義的變數,就會建立閉包。 建立閉包很容易:只需在另一個函式中定義一個函式,然後通過返回或將其傳遞給另一個函式來暴露內部函式。 即使在外部函式完成執行之後,內部函式使用的變數也可用。

在JavaScript中,你可以通過工廠函式來使用閉包建立資料隱私:

var counter = function counter() {
  var count = 0;
  return {
    getCount: function getCount() {
      return count;
    },
    increment: function increment() {
      count += 1;
    }
  };
};

在JavaScript的兩大支柱第1部分中,我描述了我們大多數人所知道的面向物件程式設計的一些主要缺點,但也指出了面向物件設計的有更光明未來的一線希望:基於原型的面向物件設計 - 不僅僅是原型委託和連結的概念,還有構建不是來自類的新例項的概念,而是來自較小物件原型的彙編。

在JavaScript的兩大支柱第2部分中,我們將討論一種完全不同的程式設計正規化,隨著現代程式語言新增越來越多的功能以減少樣板程式碼,重複和語法噪聲,這種正規化將在未來發揮更大的作用: 函數語言程式設計(FP)。

“…前進的道路有時會回來。”~ The Wise Man, “Labyrinth”

在計算機革命的最初階段,在微處理器發明幾十年之前,一個名叫Alonzo Church的人在理論電腦科學方面做了開創性的工作。 您可能聽說過他的學生和合作者,Alan Turing。 他們一起建立了一個名為Church-Turing Thesis的可計算函式理論,它描述了可計算函式的本質。 從這項工作中產生了兩種基本的計算模型,著名的圖靈機和lambda演算。

今天,圖靈機經常被引用作為現代程式語言要求的基線。 如果語言或處理器/ VM指令集可用於模擬通用圖靈機,則稱其為圖靈完備。 圖靈完備系統的第一個著名的例子是lambda演算,由Alonzo Church於1936年描述。

Lambda演算繼續激發了第一個高階程式語言之一,以及當今常用的第二古老的高階語言:Lisp。

Lisp曾經(並且仍然)在學術界極具影響力和流行,但它在生產力應用程式中也非常流行,特別是那些處理可擴充套件向量圖形等連續資料的應用程式。 Lisp最初是在1958年指定的,它仍然經常嵌入許多複雜的應用程式中。 值得注意的是,幾乎所有流行的CAD應用程式都支援AutoLISP,這是AutoCAD中使用的Lisp方言,其中包括許多專業衍生產品,它仍然是世界上使用最廣泛的CAD應用程式。

連續資料:必須被測量而不被計數的資料,並且可以在一定範圍內取任何值。不同於離散資料,可以對其進行計數和準確索引,且 所有值都來自一組特定的有效值。 例如,小提琴可以產生的頻率必須由連續資料表示,因為弦可以沿指板的任何點停止。 鋼琴可以產生的頻率是離散的,因為每根琴絃都被調到特定的音高,並且不能根據表演者的意願在不同的點停止,鋼琴鍵盤上通常有88個音符。 不可能計算小提琴演奏者可以產生的音符數量,小提琴演奏者經常通過在不同音符之間向上或向下滑動音高來利用它。

Lisp及其衍生產品誕生了一整套函數語言程式設計語言,包括Curry,Haskell,Erlang,Clojure,ML,OCaml等… 像許多這些受歡迎的語言一樣,JavaScript已經將新一代程式設計師暴露給了函數語言程式設計的概念。 函數語言程式設計非常適合JavaScript,因為JavaScript提供了一些重要的特性:一級函式,閉包和簡單的lambda語法。 JavaScript可以很容易地將函式分配給變數,將它們傳遞給其他函式,從其他函式返回函式,組合函式等等。 JavaScript還為原始型別提供了不可變值,並且這些特性使得返回新物件和陣列變得容易,而不是操縱作為引數傳入的屬性。 為什麼所有這些那麼的重要? 函數語言程式設計術語表包含大量的大詞,但從本質上講,FP的本質非常簡單; 程式主要由一些非常小的,非常可重用的,非常可預測的純函式構建。 純函式具有一些屬性,使它們非常可重用,並且對於各種應用程式非常有用: 冪等性:給定相同的輸入,無論呼叫函式的次數如何,純函式總是返回相同的輸出。 您可能聽說過用於描述HTTP GET請求的這個詞。 Idempotence是構建RESTful Web服務的一個重要特性,但它也有助於將計算與依賴於時間和操作順序的計算分開 - 這對於並行和分散式計算(想想水平擴充套件)非常有價值。 由於冪等性,當您需要對連續資料集進行操作時,純函式也非常適合。 例如,視訊或音訊中的自動淡入和淡出。 通常,它們被設計為與幀速率或採樣率無關的線性或對數曲線,並且僅在最後可能的時刻應用以產生離散資料值。 在圖形中,這類似於可縮放向量影象,如SVG,字型和Adobe Illustrator檔案。 關鍵幀或控制點相對於彼此或時間線佈置,而不是鍵入固定畫素,幀或樣本。 因為冪等函式不依賴於時間或樣本解析度,所以可以將連續資料視為無界(幾乎無限)的資料流,允許跨時間自由縮放資料,(取樣率),畫素解析度,音量, 等等。 免於副作用:純函式可以安全地應用而沒有副作用,這意味著它們不會改變任何共享狀態或可變引數,除了它們的返回值之外,它們不會產生任何可觀察的輸出,包括丟擲的異常 ,觸發事件,I / O裝置,網路,控制檯,顯示器,日誌等… 由於缺乏共享狀態和副作用,純函式不太可能相互衝突或導致程式的不相關部分中的錯誤。 換句話說,對於使用面向物件編碼的程式,純函式比沒有函式純粹性的物件產生更強的封裝保證,並且這些保證提供了許多與面向物件封裝相同的好處:在不影響程式其餘部分的情況下更改實現的能力, 自我記錄的公共介面(函式簽名),獨立於外部程式碼,能夠在檔案,模組,專案等之間自由移動程式碼等等。 函式純粹性是函數語言程式設計對面向物件程式設計中大猩猩/香蕉問題的回答:

“面嚮物件語言的問題在於它們帶有所有這些隱含的環境。 你想要一個香蕉,但你得到的是拿著香蕉的大猩猩和整個叢林。“~ Joe Armstrong

顯然,大多數程式都需要生成輸出,因此複雜的程式通常不能僅使用純函式來編寫,但是隻要它是實際有用的,那麼將函式設定為純函式是一個好主意。

用函數語言程式設計工作

函數語言程式設計提供了幾種可以重用的功能。各種實現將使用不同的集合,它們有著不同的名字。 下面這個列表主要來自Haskell文件,Haskell是一種比較流行的函式式語言,但在幾個流行的JavaScript庫中你會發現類似的函式:

列表常用工具:

  • head() - 得到第一個元素
  • tail() - 得到除了第一個元素之外的所有元素
  • last() - 得到最後一個元素
  • length() - 元素數量

謂詞/比較器(測試元素,返回布林值)

  • equal()
  • greaterThan()
  • lessThan()

列表轉換:

  • map() ([x]) -> [y] - 獲取列表x並對該列表中的每個元素應用轉換,返回新列表y
  • reverse() ([1, 2, 3]) -> [3, 2, 1]

列表搜尋:

  • find() ([x]) -> x - 獲取列表x並返回匹配謂詞的第一個元素
  • filter() ([x]) -> [y] - 取列表x並返回匹配謂詞的所有元素

列表歸約器/摺疊:

  • reduce() — ([x], function[, accumulator]) - 將函式應用於每個元素並將結果累積為單個值
  • any() - 如果任何值與謂詞匹配,則為true

迭代器/發生器/收集器(無限列表)

  • sample() - 返回連續輸入源的當前值(溫度,表格輸入,切換開關狀態等)
  • repeat() — (1) -> [1, 1, 1, 1, 1, 1,…]
  • cycle() / loop()  - 到達列表末尾時,再次回到開頭。

其中一些實用工具程式已經新增到採用ECMAScript 5 Array中。

// Using ES6 syntax. () => means function () {}
var foo = [1, 2, 3, 4, 5];
var bar = foo.map( (n) => n + 1 ); // [2, 3, 4, 5, 6]
var baz = bar.filter( (n) => n >=3 && n <=5); // [3,4,5]
var bif = baz.reduce( (n, el) => n + el); // 12

作為泛型介面的列表

您可能會注意到上面的大多數函式都是用於管理或推導列表的實用程式。 一旦開始進行大量的函數語言程式設計,您可能會開始將所有內容視為列表,列表元素,用於測試列表值的謂詞或基於列表值的轉換。 這種想法為極其可重用的程式碼奠定了基礎。

泛型是能夠處理各種不同資料型別的函式。 在JavaScript中,許多對集合進行操作的函式都是通用的。 例如,您可以對字串和陣列使用相同的函式,因為字串可以視為字元陣列。

將僅適用於單一型別的函式更改為適用於多種型別的函式的過程稱為提升。

所有這些函式能處理你提供的任意型別資料的關鍵是,資料在列表中處理,且這些列表共享相同的介面。

如何停止微觀管理一切

在面向物件和指令式程式設計中,我們對所有內容進行微觀管理。 我們微觀管理狀態,迭代計數器,使用事件發射器和回撥等事件時發生的時間。 如果我告訴你所有這些工作都是完全沒必要的 - 你可以從程式中刪除所有類別的程式碼?

反應式程式設計使用map,filter和reduce等函式實用程式來建立和處理通過系統傳播變化的資料流:因此,反應式。 輸入x更改時,輸出y會自動更新以作為響應。

在OO中,您可以設定一些物件(例如,表單輸入),並將該物件轉換為事件發射器,並設定一個事件偵聽器,該事件偵聽器會跳過某些環節並且可能在觸發事件時產生一些輸出。

使用反應式程式設計時,您會以更具說明性的方式指定資料依賴性,並且大部分繁重工作都會解除安裝到標準功能實用程式中,因此您不必一次又一次地重新發明輪子。

想象一下,每個列表都是一個流:陣列是元素順序中的值流。 表單輸入可以是每次更改時取樣的值流。 按鈕是點選流。

A stream is just a list expressed over time.

在Rx(反應式擴充套件)屬於術語中,您建立可觀察流,然後使用一組功能實用程式(如上所述)處理這些流。

想象一下,您想要檢視特定雜湊標記的所有社交媒體訂閱源。 您可以收集列表列表,按照收到的順序合併它們,然後使用上面的實用程式功能的任意組合來處理它們。 這可能看起來像這樣:

var messages = merge(twitter, fb, gplus, github)
messages.map(normalize) // convert to single message format
  .filter(selectHashtag) // cherry pick messages that use the tag
  .pipe(process.stdout) // stream output to stdout

更好的非同步

你可能聽說過promise。 promise是一個物件,它提供了一個標準介面,用於處理在使用promise時可用或不可用的值。 換句話說,promise包含了可能在將來解析的可能值。 通常,一個函式呼叫會返回一個可以解析未來值的promise:

fetchFutureStockPrices().then(becomeAMillionaire);

好吧,它不能完全像那樣,因為.then()只會在股票價格變得可用時呼叫,所以當becomeAMillionaire最終執行時,“未來”股票價格將是現在的股票價格。

promise基本上是僅發出單個值(或拒絕)的流。 Observable可以替換程式碼中的promise,並提供您可能已經習慣使用Underscore或Lo-Dash等庫的所有標準函式式實用程式。

現在是瞭解函數語言程式設計,函式純度,惰性求值等等的好處的好時機。 我保證在接下來的幾年裡你會聽到更多有關這些主題的內容。