天天看點

聲明式程式設計和指令式程式設計

先統一一下概念,我們有兩種程式設計方式:指令式和聲明式。

我們可以像下面這樣定義它們之間的不同:

指令式程式設計:指令“機器”如何去做事情(how),這樣不管你想要的是什麼(what),它都會按照你的指令實作。

聲明式程式設計:告訴“機器”你想要的是什麼(what),讓機器想出如何去做(how)。

聲明式程式設計和指令式程式設計的代碼例子

最簡單的例子就是,如果要實作頁面的跳轉的功能:

使用a标簽:<a href="/finance" />,就是聲明式程式設計。

使用js代碼:window.location.href = "/finance",就是指令式程式設計。

同樣實作一個功能,a标簽隻提供了跳轉的路徑,跳轉的動作是約定好的,而js代碼則要寫明怎麼跳轉,用什麼對象跳轉。

再舉個簡單的例子,假設我們想讓一個數組裡的數值翻倍。

我們用指令式程式設計風格實作,像下面這樣:

JavaScript

我們直接周遊整個數組,取出每個元素,乘以二,然後把翻倍後的值放入新數組,每次都要操作這個雙倍數組,直到計算完所有元素。

而使用聲明式程式設計方法,我們可以用 <code>Array.map</code> 函數,像下面這樣:

<code>map</code> 利用目前的數組建立了一個新數組,新數組裡的每個元素都是經過了傳入<code>map</code>的函數(這裡是<code>function(n) { return n*2 }</code>)的處理。

<code>map</code>函數所作的事情是将直接周遊整個數組的過程歸納抽離出來,讓我們專注于描述我們想要的是什麼(what)。注意,我們傳入map的是一個純函數;它不具有任何副作用(不會改變外部狀态),它隻是接收一個數字,傳回乘以二後的值。

在一些具有函數式程式設計特征的語言裡,對于list資料類型的操作,還有一些其他常用的聲明式的函數方法。例如,求一個list裡所有值的和,指令式程式設計會這樣做:

而在聲明式程式設計方式裡,我們使用 <code>reduce</code> 函數:

<code>reduce</code> 函數利用傳入的函數把一個 list 運算成一個值。它以這個函數為參數,數組裡的每個元素都要經過它的處理。每一次調用,第一個參數(這裡是<code>sum</code>)都是這個函數處理前一個值時傳回的結果,而第二個參數(<code>n</code>)就是目前元素。這樣下來,每此處理的新元素都會合計到<code>sum</code>中,最終我們得到的是整個數組的和。

同樣,<code>reduce</code> 函數歸納抽離了我們如何周遊數組和狀态管理部分的實作,提供給我們一個通用的方式來把一個 list 合并成一個值。我們需要做的隻是指明我們想要的是什麼?

聲明式程式設計很奇怪嗎?

如果你之前沒有聽說過<code>map</code> 和 <code>reduce</code> 函數,你的第一感覺,我相信,就會是這樣。作為程式員,我們非常習慣去指出事情應該如何運作。“去周遊這個list”,“if 這種情況 then 那樣做”,“把這個新值賦給這個變量”。當我們已經知道了如何告訴機器該如何做事時,為什麼我們需要去學習這種看起來有些怪異的歸納抽離出來的函數工具?

在很多情況中,指令式程式設計很好用。當我們寫業務邏輯,我們通常必須要寫指令式代碼,沒有可能在我們的專項業務裡也存在一個可以歸納抽離的實作。

但是,如果我們花時間去學習(或發現)聲明式的可以歸納抽離的部分,它們能為我們的程式設計帶來巨大的便捷。首先,我可以少寫代碼,這就是通往成功的捷徑。而且它們能讓我們站在更高的層面是思考,站在雲端思考我們想要的是什麼,而不是站在泥裡思考事情該如何去做。

聲明式程式設計語言:SQL

也許你還不能明白,但有一個地方,你也許已經用到了聲明式程式設計,那就是SQL。

你可以把SQL當做一個處理資料的聲明式查詢語言。完全用SQL寫一個應用程式?這不可能。但如果是處理互相關聯的資料集,它就顯的無比強大了。

像下面這樣的查詢語句:

如果我們用指令式程式設計方式實作這段邏輯:

我可沒說SQL是一種很容易懂的語言,也沒說一眼就能把它們看明白,但基本上還是很整潔的。

SQL代碼不僅很短,不不僅容易讀懂,它還有更大的優勢。因為我們歸納抽離了how,我們就可以專注于what,讓資料庫來幫我們優化how.

我們的指令式程式設計代碼會運作的很慢,因為需要周遊所有 list 裡的每個狗的主人。

而SQL例子裡我們可以讓資料庫來處理how,來替我們去找我們想要的資料。如果需要用到索引(假設我們建了索引),資料庫知道如何使用索引,這樣性能又有了大的提升。如果在此不久之前它執行過相同的查詢,它也許會從緩存裡立即找到。通過放手how,讓機器來做這些有難度的事,我們不需要掌握資料庫原理就能輕松的完成任務。

聲明式程式設計:d3.js

另外一個能展現出聲明式程式設計的真正強大之處地方是使用者界面、圖形、動畫程式設計。

開發使用者界面是有難度的事。因為有使用者互動,我們希望能建立漂亮的動态使用者互動方式,通常我們會用到大量的狀态聲明和很多相同作用的代碼,這些代碼實際上是可以歸納提煉出來的。

d3.js 裡面一個非常好的聲明時歸納提煉的例子就是它的一個工具包,能夠幫助我們使用JavaScript和SVG來開發互動的和動畫的資料可視化模型。

第一次(或第5次,甚至第10次)你開發d3程式時可能會頭大。跟SQL一樣,d3是一種可視化資料操作的強大通用工具,它能提供你所有how方法,讓你隻需要說出你想要什麼。

下面是一個例子。這是一個d3可視化實作,它為<code>data</code>數組裡的每個對象畫一個圓。為了示範這個過程,我們每秒增加一個圓。

裡面最有趣的一段代碼是:

沒有必要完全了解這段代碼都幹了什麼(你需要一段時間去領會),但關鍵點是:

首先我們收集了svg裡所有的圓,然後把data數組資料綁定到對象裡。

D3 對每個圓都綁定了那些點資料有一個關系表。最初我們隻有兩個點,沒有圓,我們使用<code>.enter()</code>方法擷取資料點。這裡,我們的意圖是畫一個圓,中心是<code>x</code> 和 <code>y</code>,初始值是 <code>0</code>,半秒後變換成半徑為 <code>5</code>。

為什麼我說這很有意思?

從頭再看一遍代碼,想一想,我們是在聲明我們想要的圖案是什麼樣子,還是在說如何作圖。你會發現這裡根本沒有關于how的代碼。我們隻是在一個相當高的層面描述我們想要的是什麼:

我要畫圓,圓心在data資料裡,當增加新圓時,用動畫表示半徑的增加。

這太神奇了,我們沒有寫任何循環,這裡沒有狀态管理。畫圖操作通常是很難寫,很麻煩,很讓人讨厭,但這裡,d3歸納提取了一些常用的操作,讓我們專注于描述我們想要的是什麼。

現在再看,d3.js 很容易了解嗎?不是,它絕對需要你花一段時間去學習。而學習的過程基本上需要你放棄去指明如何做事的習慣,而去學會如何描述我想要的是什麼。

最初,這可能是很困難的事,但經過一些時間的學習後,一些神奇的事情發生了——你變得非常非常有效率了。通過歸納提取how,d3.js 能讓你真正的專注說明你想要看到的是什麼,讓你在一個個更高的層面解決問題,解放你的創作力。

聲明式程式設計的總結

聲明式程式設計讓我們去描述我們想要的是什麼,讓底層的軟體/計算機/等去解決如何去實作它們。

在很多情況中,就像我們看到的一樣,聲明式程式設計能給我們的程式設計帶來真正的提升,通過站在更高層面寫代碼,我們可以更多的專注于what,而這正是我們開發軟體真正的目标。

問題是,程式員習慣了去描述how,這讓我們感覺很好很舒服——強力——能夠控制事情的發生發展,不放走任何我們不能看見不能了解的處理過程。

有時候這種緊盯着how不放的做法是沒問題的。如果我需要對代碼進行更高性能的優化,我需要對what進行更深一步的描述來指導how。有時候對于某個業務邏輯沒有任何可以歸納提取的通用實作,我們隻能寫指令式程式設計代碼。

但大多數時候,我們可以、而且應該尋求聲明式的寫代碼方式,如果沒有發現現成的歸納提取好的實作,我們應該自己去建立。起初這會很難,必定的,但就像我們使用SQL和D3.js, 我們會長期從中獲得巨大的回報!

繼續閱讀