走進函數式編程

最近在看《CoderAtWorks》時發現編程界的大佬們幾乎都對函數式編程很是推崇,因而就很是好奇函數式編程究竟是什麼東西,搜索引擎查了一堆資料,算是半懂了吧,因而就藉此總結下學習的東西。程序員

手機上寫Markdown絕對是一種折磨,排版仍是等回家再說吧。編程

1.淵源

20世紀30年代普林斯頓大學有四位學者, 艾倫·圖靈 、 約翰·馮·諾伊曼 、 庫爾特·哥德爾 、 阿隆佐·邱奇 ,他們都對形式系統感興趣,相對於現實世界,他們更關心如何解決抽象的數學問題。而他們的問題都有這麼一個共同點:都在嘗試解答關於計算的問題。諸如:若是有一臺擁有無窮計算能力的超級機器,能夠用來解決什麼問題?它能夠自動的解決這些問題嗎?是否是仍是有些問題解決不了,若是有的話,是爲何?若是這樣的機器採用不一樣的設計,它們的計算能力相同嗎?數組

在與這些人的合做下,阿隆佐設計了一個名爲 lambda演算 的形式系統。這個系統實質上是爲其中一個超級機器設計的編程語言。在這種語言裏面,函數的參數是函數,返回值也是函數。這種函數用希臘字母lambda(λ),這種系統所以得名。有了這種形式系統,阿隆佐終於能夠分析前面的那些問題而且可以給出答案了。閉包

除了阿隆佐·邱奇,艾倫·圖靈也在進行相似的研究。他設計了一種徹底不一樣的系統(後來被稱爲圖靈機),並用這種系統得出了和阿隆佐類似的答案。到了後來人們證實了圖靈機和 lambda演算 的能力是同樣的。架構

1949年第一臺電子離散變量自動計算機誕生並取得了巨大的成功。它是馮·諾伊曼設計架構的第一個實例,也是一臺現實世界中實現的圖靈機。相比他的這些同事,那個時候阿隆佐的運氣就沒那麼好了。併發

到了50年代末,一個叫John McCarthy的MIT教授(他也是普林斯頓的碩士)對阿隆佐的成果產生了興趣。1958年他發明了一種列表處理語言(Lisp),這種語言是一種阿隆佐lambda演算在現實世界的實現,並且它能在馮·諾伊曼計算機上運行!不少計算機科學家都認識到了Lisp強大的能力。1973年在MIT人工智能實驗室的一些程序員研發出一種機器,並把它叫作Lisp機。因而阿隆佐的 lambda演算 也有本身的硬件實現了!編程語言

2.定義

Lisp 誕生以後,新的函數式編程語言層出不窮,例如 Erlang 、 clojure 、 Scala 、 F# 等等。目前最當紅的 Python 、 Ruby 、 Javascript ,對函數式編程的支持都很強,就連老牌的面向對象的 Java 、面向過程的 PHP ,都忙不迭地加入對匿名函數的支持。函數式編程

簡單說,"函數式編程"是一種"編程範式"(programming paradigm),也就是如何編寫程序的方法論。函數

它屬於"結構化編程"的一種,主要思想是把運算過程儘可能寫成一系列嵌套的函數調用。舉例來講,如今有這樣一個數學表達式:工具

'''

(1 + 2) * 3 - 4

'''

傳統的過程式編程,可能這樣寫:

'''

var a = 1 + 2;

var b = a * 3;

var c = b - 4;

'''

函數式編程要求使用函數,咱們能夠把運算過程定義爲不一樣的函數,而後寫成下面這樣:

'''

var res = subtract(multiply(add(1,2), 3), 4);

'''

這就是函數式編程。

3.特色

函數是一等公民

所謂"第一等公民"(first class),指的是函數與其餘數據類型同樣,處於平等地位,能夠賦值給其餘變量,也能夠做爲參數,傳入另外一個函數,或者做爲別的函數的返回值。

舉例來講,下面代碼中的print變量就是一個函數,能夠做爲另外一個函數的參數。

'''

var print = function(i){ console.log(i);};

[1,2,3].forEach(print);

'''

不可改變量

在函數式編程中,咱們一般理解的變量在函數式編程中也被函數代替了:在函數式編程中變量僅僅表明某個表達式。這裏所說的‘變量’是不能被修改的。全部的變量只能被賦一次初值。在Java中就意味着每個變量都將被聲明爲final(若是你用C++,就是const)。在函數式編程中,沒有非final的變量。

final int i = 5;

final int j = i + 3;

無狀態

若是變量不能夠改變,那麼狀態如何存儲,這個不用擔憂,函數式編程中狀態經過函數來保存,若是你須要保存一個狀態一段時間而且時不時的修改它,那麼你能夠編寫一個遞歸函數。舉個例子,試着寫一個函數,用來反轉一個字符串。

function reverse(String arg) {

if(arg.length == 0) {

return arg;

} else {

return reverse(arg.substring(1, arg.length)) + arg.substring(0, 1);

}

}

因爲使用了遞歸,函數式語言的運行速度比較慢,這是它長期不能在業界推廣的主要緣由。

技術

map & reduce

map 和 reduce 開篇時已經提到過,他們是對一個集合最經常使用的操做。

map 接受一個集合和一個函數f,集合中每一個元素都映射到函數f上,並返回一個新的集合,簡單實現以下(詳見 mdn map polyfill ):

function map(arr, callback){

var l = arr && arr.length || 0,

out = [];

for (var i = 0; i < l; i++) {

out[i] = callback(arr[i]);

}

return out;

}

reduce 接受一個集合和一個函數f,而後將f映射到數組的相鄰的兩個元素上,簡單實現以下(詳見 mdn reduce polyfill ):

function reduce(arr, callback, b){

var l = arr && arr.length || 0,

x = 0;

b = b || 0;

for (var i = 0; i < l; i++) {

x = callback(arr[i], x);

}

return x;

}

柯里化

柯里化就是把一個函數的多個參數分解成多個函數, 而後把函數多層封裝起來,每層函數都返回一個函數去接收下一個參數這樣,能夠簡化函數的多個參數。

例如要計算一個數的平方,能夠先實現一個計算任意整數次冪的函數,而後調用接口實現計算一個數的平方:

function pow(base, p) {/計算base的p次方/}

function square(a) {

return pow(a, 2);

}

柯里化就是這麼簡單:一種能夠快速且簡單的實現函數封裝的捷徑。咱們能夠更專一於本身的設計,編譯器則會爲你編寫正確的代碼!何時使用currying呢?很簡單,當你想要用適配器模式(或是封裝函數)的時候,就是用currying的時候。對於函數編程來講,適配器模式就是多餘的。

惰性求值

在指令式語言中如下代碼會按順序執行,因爲每一個函數都有可能改動或者依賴於其外部的狀態,所以必須順序執行。先是計算 somewhatLongOperation1 ,而後到 somewhatLongOperation2 ,最後執行 concatenate 。假如把 concatenate 換成另一個函數,這個函數中有條件判斷語句並且實際上只會須要兩個參數中的其中一個,那麼就徹底沒有必要執行計算另一個參數的函數了!

var s1 = somewhatLongOperation1();

var s2 = somewhatLongOperation2();

var s3 = concatenate(s1, s2);

函數式語言就不同了。只有到了執行須要 s1 、 s2 做爲參數的函數的時候,才真正須要執行這兩個函數。因而在 concatenate 這個函數沒有執行以前,都沒有須要去執行這兩個函數:這些函數的執行能夠一直推遲到 concatenate() 中須要用到s1和s2的時候。

惰性求值是十分強大的技術,可是須要編譯器的支持。

高階函數

高階函數就是函數當參數,把傳入的函數作一個封裝,而後返回這個封裝函數。例如咱們要實現一個計算1和任意數字的和的函數:

var partAdd = function(p1){

this.add = function (p2){

return p1 + p2;

};

return add;

};

var add = partAdd(1);

add(2); // 3

執行 partAdd(1) 時返回的任然是一個函數,當再次傳入第二個參數時,就能夠計算出和了。

上面的例子只是爲了理解高階函數,實際運用以下例所示:

var add = function(a,b){

return a + b;

};

function math(func,array){

return func(array[0],array[1]);

}

math(add,[1,2]); // 3

尾調用優化

尾調用的概念很是簡單,一句話就能說清楚,就是指某個函數的最後一步是調用另外一個函數。

function f(x){

return g(x);

}

上面代碼中,函數f的最後一步是調用函數g,這就叫尾調用。

如下兩種狀況,都不屬於尾調用。

// 狀況一

function f(x){

let y = g(x);

return y;

}

// 狀況二

function f(x){

return g(x) + 1;

}

上面代碼中,狀況一是調用函數g以後,還有別的操做,因此不屬於尾調用,即便語義徹底同樣。狀況二也屬於調用後還有操做,即便寫在一行內。

咱們知道函數調用會在內存造成一個"調用記錄",又稱"調用幀"(call frame),保存調用位置和內部變量等信息。多層次的調用記錄造成了調用棧。尾調用因爲是函數的最後一步操做,因此不須要保留外層函數的調用記錄,由於調用位置、內部變量等信息都不會再用到了,只要直接用內層函數的調用記錄,取代外層函數的調用記錄就能夠了。

function f() {

let m = 1;

let n = 2;

return g(m + n);

}

f();

// 等同於

function f() {

return g(3);

}

f();

// 等同於

g(3);

函數調用自身,稱爲遞歸。若是尾調用自身,就稱爲尾遞歸。基於尾調用優化的原理,咱們能夠對尾遞歸進行優化。遞歸須要保存大量的調用記錄,很容易發生棧溢出錯誤,若是使用尾遞歸優化,將遞歸變爲循環,那麼只須要保存一個調用記錄,這樣就不會發生棧溢出錯誤了。

例如計算階乘的函數:

// 不是尾遞歸,沒法優化

function factorial(n) {

if (n === 1) return 1;

return n * factorial(n - 1);

}

// 尾遞歸,能夠優化

function factorial(n, total) {

if (n === 1) return total;

return factorial(n - 1, n * total);

}

目前的ES5中並無規定尾調用優化,可是ES6中明確規定了必須實現尾調用優化,也就是ES6中只要使用尾遞歸,就不會發生棧溢出。因此 對於遞歸函數儘可能改寫爲尾遞歸形式 。

閉包

目前爲止關於函數式編程各類功能的討論都只侷限在「純」函數式語言範圍內。不少這樣的語言都不要求全部的變量必須爲final,能夠修改他們的值。也不要求函數只能依賴於它們的參數,而是能夠讀寫函數外部的狀態。同時這些語言又包含了函數編程的特性,如高階函數。與在lambda演算限制下將函數做爲參數傳遞不一樣,在指令式語言中要作到一樣的事情須要支持一個有趣的特性,人們常把它稱爲lexical closure。

看以下例子,雖然外層的 makePowerFn 函數執行完畢,棧上的調用幀被釋放,可是堆上的做用域並不被釋放,所以 power 依舊能夠被 powerFn 函數訪問,這樣就造成了閉包:

function makePowerFn(power) {

function powerFn(base) {

return pow(base, power);

}

return powerFn;

}

var square = makePowerFn(2);

square(3); // 9

優勢

代碼簡潔,易於理解

函數式編程大量使用函數,減小了代碼的重複,所以程序比較短,開發速度較快。Paul Graham在《黑客與畫家》一書中寫道:一樣功能的程序,極端狀況下,Lisp代碼的長度多是C代碼的二十分之一。

函數式編程的自由度很高,能夠寫出很接近天然語言的代碼。例如前文提到的 (1 + 2) * 3 - 4 的例子,寫成函數時:

add(1,2).multiply(3).subtract(4);

容易調試

由於函數式編程中的每一個符號都是 final 的,因而沒有什麼函數會有反作用。誰也不能在運行時修改任何東西,也沒有函數能夠修改在它的做用域以外修改什麼值給其餘函數繼續使用(在指令式編程中能夠用類成員或是全局變量作到)。這意味着決定函數執行結果的惟一因素就是它的返回值,而影響其返回值的惟一因素就是它的參數。

若是一段FP程序沒有按照預期設計那樣運行,調試的工做幾乎不費吹灰之力。這些錯誤是百分之一百能夠重現的,由於FP程序中的錯誤不依賴於以前運行過的不相關的代碼。

併發

函數式編程不須要考慮"死鎖"(deadlock),由於它不修改變量,因此根本不存在"鎖"線程的問題。沒必要擔憂一個線程的數據,被另外一個線程修改,因此能夠很放心地把工做分攤到多個線程,部署"併發編程"(concurrency)。

仍是以前的例子:

var s1 = somewhatLongOperation1();

var s2 = somewhatLongOperation2();

var s3 = concatenate(s1, s2);

因爲 s1 和 s2 互不干擾,不會修改變量,誰先執行是無所謂的,因此能夠放心地增長線程,把它們分配在兩個線程上完成。其餘類型的語言就作不到這一點,由於s1可能會修改系統狀態,而s2可能會用到這些狀態,因此必須保證 s2 在 s1 以後運行,天然也就不能部署到其餘線程上了。

熱部署

函數式編程中全部狀態就是傳給函數的參數,而參數都是儲存在棧上的。這一特性讓軟件的熱部署變得十分簡單。只要比較一下正在運行的代碼以及新的代碼得到一個diff,而後用這個diff更新現有的代碼,新代碼的熱部署就完成了。其它的事情有FP的語言工具自動完成! Erlang 語言早就證實了這一點,它是瑞典愛立信公司爲了管理電話系統而開發的,電話系統的升級固然是不能停機的。

相關文章
相關標籤/搜索