左耳朵耗子:編程的本質是什麼?

接收程序員的技術早餐

左耳朵耗子:编程的本质是什么?

作者|陳皓

知其然,知其所以然。理解了編程的本質,有助於你成為一個更高效更優秀的程序員。

前段時間,我寫了一系列編程範式遊記的文章,有很多讀者紛紛留言討論相關的話題,還有讀者留言表示對這個主題感興趣,希望我能推薦一些學習資料。這些內容基本都會在我的極客時間專欄《左耳聽風》中和大家分享。

掃描二維碼訂閱《左耳聽風》

我們講了各式各樣的不同語言的編程範式,從 C 語言的泛型,講到 C++ 的泛型,再講到函數式的 Map/Reduce/Filter,以及 Pipeline 和 Decorator,還有面向對象的多態通過依賴於接口而不是實現的橋接模式、策略模式和代理模式,以及面向對象的 IoC,還有 JavaScript 的原型編程在運行時對對象原型進行修改,以及 Go 語言的委託模式……

所有的這一切,不知道你是否看出一些端倪,或是其中的一些共性來了?

兩篇論文

1976 年,瑞士計算機科學家,Algol W,Modula,Oberon 和 Pascal 語言的設計師 Niklaus Emil Wirth 寫了一本非常經典的書《Algorithms + Data Structures = Programs》(鏈接為 1985 年版) ,即算法 + 數據結構 = 程序。

這本書主要寫了算法和數據結構的關係,這本書對計算機科學的影響非常深遠,尤其在計算機科學的教育中。

1979 年,英國邏輯學家和計算機科學家 Robert Kowalski 發表論文 Algorithm = Logic + Control,並且主要開發“邏輯編程”相關的工作。

Robert Kowalski 是一位邏輯學家和計算機科學家,從 20 世紀 70 年代末到整個 80 年代致力於數據庫的研究,並在用計算機證明數學定理等當年的重要應用上頗有建樹,尤其是在邏輯、控制和算法等方面提出了革命性的理論,極大地影響了數據庫、編程語言,直至今日的人工智能。

Robert Kowalski 在這篇論文裡提到:

An algorithm can be regarded as consisting of a logic component, which specifies the knowledge to be used in solving problems, and a control component, which determines the problem-solving strategies by means of which that knowledge is used. The logic component determines the meaning of the algorithm whereas the control component only affects its efficiency. The efficiency of an algorithm can often be improved by improving the control component without changing the logic of the algorithm. We argue that computer programs would be more often correct and more easily improved and modified if their logic and control aspects were identified and separated in the program text.

翻譯過來的意思大概就是:

任何算法都會有兩個部分, 一個是 Logic 部分,這是用來解決實際問題的。另一個是 Control 部分,這是用來決定用什麼策略來解決問題。Logic 部分是真正意義上的解決問題的算法,而 Control 部分只是影響解決這個問題的效率。程序運行的效率問題和程序的邏輯其實是沒有關係的。我們認為,如果將 Logic 和 Control 部分有效地分開,那麼代碼就會變得更容易改進和維護。

注意,最後一句話是重點——如果將 Logic 和 Control 部分有效地分開,那麼代碼就會變得更容易改進和維護。

編程的本質

兩位老先生的兩個表達式:

  • Programs = Algorithms + Data Structures

  • Algorithm = Logic + Control

第一個表達式傾向於數據結構和算法,它是想把這兩個拆分,早期都在走這條路。他們認為,如果數據結構設計得好,算法也會變得簡單,而且一個好的通用的算法應該可以用在不同的數據結構上。

第二個表達式則想表達,數據結構不復雜,複雜的是算法,也就是我們的業務邏輯是複雜的。我們的算法由兩個邏輯組成,一個是真正的業務邏輯,另外一種是控制邏輯。程序中有兩種代碼,一種是真正的業務邏輯代碼,另一種代碼是控制我們程序的代碼,叫控制代碼,這根本不是業務邏輯,業務邏輯不關心這個事情。

算法的效率往往可以通過提高控制部分的效率來實現,而無須改變邏輯部分,也就無無須改變算法的意義。舉個階乘的例子: X(n)!= X(n) X(n-1) X(n-2) X(n-3) ... 3 2 1。邏輯部分用來定義階乘:1) 1 是 0 的階乘; 2)如果 v 是 x 的階乘,且 u=v(x+1),那麼 u 是 x+1 的階乘。

用這個定義,既可以從上往下地將 x+1 的階乘縮小為先計算 x 的階乘,再將結果乘以 1(recursive,遞歸),也可以由下而上逐個計算一系列階乘的結果(iteration,遍歷)。

控制部分用來描述如何使用邏輯。最粗略的看法可以認為“控制”是解決問題的策略,而不會改變算法的意義,因為算法的意義是由邏輯決定的。對同一個邏輯,使用不同控制,所得到的算法,本質是等價的,因為它們解決同樣的問題,並得到同樣的結果。

因此,我們可以通過邏輯分析,來提高算法的效率,保持它的邏輯,而更好地使用這一邏輯。比如,有時用自上而下的控制替代自下而上,能提高效率。而將自上而下的順序執行改為並行執行,也會提高效率。

總之,通過這兩個表達式,我們可以得出:

Program = Logic + Control + Data Structure

前面講了這麼多的編程範式,或是程序設計的方法。其實,我們都是在圍繞著這三件事來做的。比如:

  • 就像函數式編程中的 Map/Reduce/Filter,它們都是一種控制。而傳給這些控制模塊的那個 lambda 表達式才是我們要解決的問題的邏輯,它們共同組成了一個算法。最後,我再把數據放在數據結構裡進行處理,最終就成為了我們的程序。

  • 就像我們 Go 語言的委託模式的那個 Undo 示例一樣。Undo 這個事是我們想要解決的問題,是 Logic,但是 Undo 的流程是控制。

  • 就像我們面向對象中依賴於接口而不是實現一樣,接口是對邏輯的抽象,真正的邏輯放在不同的具現類中,通過多態或是依賴注入這樣的控制來完成對數據在不同情況下的不同處理。

如果你再仔細地結合我們之前講的各式各樣的編程範式來思考上述這些概念的話,你是否會覺得,所有的語言或編程範式都在解決上面的這些問題。也就下面的這幾個事。

  • Control 是可以標準化的。比如:遍歷數據、查找數據、多線程、併發、異步等,都是可以標準化的。

  • 因為 Control 需要處理數據,所以標準化 Control,需要標準化 Data Structure,我們可以通過泛型編程來解決這個事。

  • 而 Control 還要處理用戶的業務邏輯,即 Logic。所以,我們可以通過標準化接口 / 協議來實現,我們的 Control 模式可以適配於任何的 Logic。

上述三點,就是編程範式的本質。

  • 有效地分離 Logic、Control 和 Data 是寫出好程序的關鍵所在!

  • 有效地分離 Logic、Control 和 Data 是寫出好程序的關鍵所在!

  • 有效地分離 Logic、Control 和 Data 是寫出好程序的關鍵所在!

我們在寫代碼當中,就會看到好多這種代碼,會把控制邏輯和業務邏輯放在一塊。裡面有些變量和流程是跟業務相關的,有些是不相關的。業務邏輯決定了程序的複雜度,業務邏輯本身就複雜,你的代碼就不可能寫得簡單。

Logic,它是程序複雜度的的下限,然後,我們為了控制程序,需要再搞出很多控制代碼,於是 Logic+Control 的相互交織成為了最終的程序複雜度。

把邏輯和控制混淆的示例

我們來看一個示例,這是我在 leetcode 上做的一道題,這是通配符匹配,給兩個字符串匹配。需求如下:

通配符匹配 isMatch("aa","a") → false isMatch("aa","aa") → true isMatch("aaa","aa") → false isMatch("aa", "*") → true isMatch("aa", "a*") → true isMatch("ab", "?*") → true isMatch("aab", "c*a*b") → false

現在你再看看我寫出來的代碼:

bool isMatch(const char *s, const char *p) { const char *last_s = ; const char *last_p = ; while ( *s != '\0' ) { if ( *p == '*' ) { p++; if ( *p == '\0' ) return true; last_s = s; last_p = p; } else if ( *p == '?' || *s == *p ) { s++; p++; } else if ( last_s != ) { p = last_p; s = ++last_s; } else { return false; } } while ( *p == '*' ) p++; return *p == '\0'; }

我也不知道我怎麼寫出來的,好像是為了要通過,我需要關注於性能,你看,上面這段代碼有多亂。如果我不寫註釋你可能都看不懂了。就算我寫了註釋以後,你敢改嗎?你可能連動都不敢動(哈哈)。上面這些代碼裡面很多都不是業務邏輯,是用來控制程序的邏輯。

業務邏輯是相對複雜的,但是控制邏輯跟業務邏輯交叉在一塊,雖然代碼寫得不多,但是這個代碼已經夠複雜了。兩三天以後,我回頭看,我到底寫的什麼,我也不懂,為什麼會寫成這樣?我當時腦子是怎麼想的?我完全不知道。我現在就是這種感覺。

那麼,怎麼把上面那段代碼寫得更好一些呢?

  • 首先,我們需要一個比較通用的狀態機(NFA,非確定有限自動機,或者 DFA,確定性有限自動機),來維護匹配的開始和結束的狀態。這屬於 Control。

  • 如果我們做得好的話,還可以抽像出一個像程序的文法分析一樣的東西。這也是 Control。

  • 然後,我們把匹配 * 和 ? 的算法形成不同的匹配策略。

這樣,我們的代碼就會變得漂亮一些了,而且也會快速一些。

這裡有篇正則表達式的高效算法的論文 Regular Expression Matching Can Be Simple And Fast,推薦你讀一讀,裡面有相關的實現,我在這裡就不多說了。

https://swtch.com/~rsc/regexp/regexp1.html

這裡,想說的程序的本質是 Logic+Control+Data,而其中,Logic 和 Control 是關鍵。注意,這個和系統架構也有相通的地方,邏輯是你的業務邏輯,邏輯過程的抽象,加上一個由術語表示的數據結構的定義,控制邏輯跟你的業務邏輯是沒關係的,你控制它執行。

控制一個程序流轉的方式,即程序執行的方式,並行還是串行,同步還是異步,以及調度不同執行路徑或模塊,數據之間的存儲關係,這些和業務邏輯沒有關係。

左耳朵耗子:编程的本质是什么?

如果你看過那些混亂不堪的代碼,你會發現其中最大的問題是我們把這 Logic 和 Control 糾纏在一起了,所以會導致代碼很混亂,難以維護,Bug 很多。絕大多數程序複雜的原因就是這個問題。就如同下面這幅圖中表現的情況一樣。

左耳朵耗子:编程的本质是什么?

再來一個簡單的示例

這裡給一個簡單的示例。

下面是一段檢查用戶表單信息常見的代碼,我相信這樣的代碼你見得多了。

function check_form_x { var name = $('#name').val; if ( == name || name.length <= 3) { return { status : 1, message: 'Invalid name' }; } var password = $('#password').val; if ( == password || password.length <= 8) { return { status : 2, message: 'Invalid password' }; } var repeat_password = $('#repeat_password').val; if (repeat_password != password.length) { return { status : 3, message: 'Password and repeat password mismatch' }; } var email = $('#email').val; if (check_email_format(email)) { return { status : 4, message: 'Invalid email' }; } ... return { status : 0, message: 'OK' }; } 

但其實,我們可以做一個 DSL+ 一個 DSL 的解析器,比如:

var meta_create_user = { form_id : 'create_user', fields : [ { id : 'name', type : 'text', min_length : 3 }, { id : 'password', type : 'password', min_length : 8 }, { id : 'repeat-password', type : 'password', min_length : 8 }, { id : 'email', type : 'email' } ] }; var r = check_form(meta_create_user);

這樣,DSL 的描述是“Logic”,而我們的 check_form 則成了“Control”,代碼就非常好看了。

小 結

代碼複雜度的原因:

  • 業務邏輯的複雜度決定了代碼的複雜度;

  • 控制邏輯的複雜度 + 業務邏輯的複雜度 ==> 程序代碼的混亂不堪;

  • 絕大多數程序複雜混亂的根本原因:業務邏輯與控制邏輯的耦合。

如何分離 control 和 logic 呢?我們可以使用下面的這些技術來解耦。

  • State Machine

    • 狀態定義

    • 狀態變遷條件

    • 狀態的 action

  • DSL – Domain Specific Language

    • HTML,SQL,Unix Shell Script,AWK,正則表達式……

  • 編程範式

    • 面向對象:委託、策略、橋接、修飾、IoC/DIP、MVC……

    • 函數式編程:修飾、管道、拼裝

    • 邏輯推導式編程:Prolog

這就是編程的本質:

  • Logic 部分才是真正有意義的(What)

  • Control 部分只是影響 Logic 部分的效率(How)

以下是《編程範式遊記》系列文章的目錄,方便你瞭解這一系列內容的全貌。這一系列文章中代碼量很大,很難用音頻體現出來,所以沒有錄製音頻,還望諒解。

  • 編程範式遊記(1)- 起源

  • 編程範式遊記(2)- 泛型編程

  • 編程範式遊記(3)- 類型系統和泛型的本質

  • 編程範式遊記(4)- 函數式編程

  • 編程範式遊記(5)- 修飾器模式

  • 編程範式遊記(6)- 面向對象編程

  • 編程範式遊記(7)- 基於原型的編程範式

  • 編程範式遊記(8)- Go 語言的委託模式

  • 編程範式遊記(9)- 編程的本質

  • 編程範式遊記(10)- 邏輯編程範式

  • 編程範式遊記(11)- 程序世界裡的編程範式

專屬福利

現在訂閱,立享福利:

福利一:原價¥199/ 年,極客時間新用戶註冊立減¥30

今日薦文
左耳朵耗子:编程的本质是什么?

分佈式系統架構經典資料


分享到:


相關文章: