編程改革
[本文英文原文鏈接: I want to fix programming ]
本文的作者 Jon BeltranDeHeredia
本文的作者 Jon Beltran 是一個西班牙程序員,作家,企業家,大學時輟學專職做游戲開發,他目前主要經營 Symnum Systems 公司,開發 ViEmu
和 Codekana 這兩個開發工具。
軟件編程出問題了。出大問題了。如今的這種編程方式讓人如此不堪忍受,以至于讓人想吐。數年來我一直在說我痛恨編程。過去的 20 年,我一直是個全職的軟件開發者,目前也是,我沒后悔過,我仍然熱愛著我可以用編程來做的事情。可仍然,我痛恨編程。
現在的編碼方式是一種讓大腦自殘的方式。編寫過程中的每一步,你都可能使程序崩潰——耗盡了內存,訪問了錯誤的指針或引用,或進入了死循環。毫無疑問,編程給人的感覺就像赤腳走在到處是碎玻璃的地板上。一小寸誤差的落腳距離,喀嚓,你就損失了半個腳趾頭。
這種編程方式的每一步,在每一個語句里,每一行代碼里,函數調用或過程里,如果你想寫出能用的代碼,你必須要考慮整個程序中所有的不同的、可能 的狀態。這些狀態是不可見的,你不可能給它們明確的定義。事實就是這樣。一直是這樣。包括現存的所有的語言。這就是為什么 100% 的代碼測試覆蓋率也不能保證代碼里沒有 bug,永遠也不可能。這也是為什么差程序員不能變好的原因:根本沒有一個結構化的方式讓他們考慮到所有這些可能的情況。
(順便提一下,當遇到了多線程程序時,這種情況會惡化 1000 倍——不是變得更好,而是更壞。)
問題的原因就在于,代碼被寫出來的基本方式就是錯誤的。完全是錯誤的。你寫出了一行行的指令,一步一步,看起來你把程序驅動到了一個想要的狀態。但每一步都是相互獨立的,只有編譯器/解釋器能獨自的理解它們,你基本上是很容易把事情做錯,而不是做對。
函數式編程也許是一種解決方案,我思考了很長時間,做了認真的研究。Lisp,Haskell。Lambda 計算。函數式的編程方式確實給常規的命令式或面向對象的編程方法帶來了不少改進。但這仍不能根本解決問題。它仍然是由很多無聯系的簡單步驟組成,痛苦的計 算出輸出結果。
這種編碼方式關鍵是什么地方出了問題?關鍵地方就在于,你不是在表達你想要什么。你表達的是需要采用什么步驟。試想一下,你讓朋友從冰箱里拿出一瓶啤酒,一步一步來,每一步都如機器人般的刻板,每一步都不關系到下一步做什么。這是在折磨一個人。極有可能造成災難性的失敗。這跟現在的編程方式是完全一樣的。
程序庫(lib)能帶來有用的幫助,但它們只是為應付上層特定需求的快捷方式。它們解決不了真正的問題。
最近出現了一篇非常有趣的 John Carmack 所寫的文章,講的是靜態代碼檢查,他引用了一條說的非常正確的微博,是 Dave Revell 寫的關于代碼檢查的:
“我越用靜態代碼分析來檢查代碼,我越發現計算機的強大之處。”
一種觀點
那么,應該如何編程?讓我們來舉個簡單的例子:排序。假設你有一個輸入序列,讓我們稱它,呃哼,輸入值。假設它有幾個元素。現在我們要計算出一個新的序列,稱它為輸出值,里面要包含有相同的元素,但元素是經過升序排序過的。我們如何去做?
傳統的方法有冒泡排序,快速排序,shell 排序,插入排序,等。這些都是能夠讓我們對一個序列進行排序的方法。例如,冒泡排序:
def bubble_sort ( input, output ):
output = input # start with the unsorted list
swapped = True
while swapped:
swapped = False
for i = 1 to length (output) - 1:
if output[i+1] > output[i]:
swap ( output[i+1], output[i] )
swapped = True
非常的直接。但如果打算去寫出這種排序的代碼,你仍然會犯錯誤!你可能會在交換兩個元素時忘記了把“swapped”參數設置成 true,或者更典型的,你可能在循環計數時犯下忘記減一的錯誤。
這就是我為什么要說這種編程方式有問題的原因:排序是一種很簡單的可以掌握和描述清楚的概念,可是,用代碼去實現它卻是復雜的,充滿了陷阱,隨時造成程序的崩潰,或輸出錯誤的結果。一件難事!
有人可能會寫出一種函數式的上面的算法,但相似之處會是非常明顯的:沒有副作用,可仍然包含完成這個任務所需的很多步驟。遞歸也許會比迭代更優雅(呃哼),但它并不是本質上更好。
那么,對于一個排序操作,它真正的代碼應該是什么樣的呢?這多年來,我慢慢總結出,它應該是一種類似這樣的東西(請原諒,這些是只是一些偽代碼,一種不存在的編程方式):
def sort (input[]) = output[]:
one_to_one_equal (input, output)
foreach i, j in 1..len (output):
where i < j:
output[i] <= output[j]
讓我對它做一些解釋:這第一行對sort的定義是說,在輸出序列和輸入序列之間已經存在一種 1 對 1 的“關系”。我們下文中會介紹在one_to_one_equal的定義中如何實現這個。這樣一來輸入和輸出序列中確保了相同的元素。它在空間上定義出來可能的答案。
第二,關鍵點,這下面的行指明,對于輸出序列中的每一對元素,當第一個的索引低于第二個的索引時,它的值也是較小或相等。這本質上就指明了輸出序列上排序過了。它定義了解決方案中的一種可能的答案。
這是如此的簡單。排序函數只是說明排序的結果,而不是如何做。它描述了輸出數據,以及相關的輸入數據的特征,它把如何能達到這個結果的任務交給了編譯器。
無庸置疑,這存在兩個關鍵問題:
- 首先,編譯器如何能完成這個任務?真的有這種可能嗎?在將來的文章里,我將會告訴你這是可能的,真的可能,編譯器甚至能知道采用什么樣的算法來獲得這樣的結果。
- 第二個問題是,如果把它應用到更復雜的情況中?我還是能向你展示,這種方式完全可以應用到任何的所有的編程和計算任務中,它只是一種更簡單,更有效,更能避免錯誤的編程方式!
我曾經想不公開這種技術,將來成立一個公司來實現這種思想,但多種環境因素使我重新思考這個計劃。現在我向大家分享了我的認識,想看看事情會如何發展。請關注本系列中的下幾篇文章。
尾注
在本系列的后續文章中我會做深入講解,這里只稍微提一點。這個one_to_one_equal函數在這種理想化的語言中將會是一個“標準庫函數”,它多少看起來應該像這個樣子,像下面這個基本邏輯:
def one_to_one_equal (output[], input[]) = c:
c = relations (input[i], output[j])
foreach x = input[i]: len (c(x,)) = 1
foreach x = output[i]: len (c(,x)) = 1
foreach x (a,b)=c[i]: a == b
讓我來解釋一下:這第一行的定義是說,在輸入和輸出序列中的元素間有一個 1 對 1 的“關系”集合。
這第二和第三行指明,對于每一個輸入和輸出序列的元素,在集合“c”中都有一個單一的關系從屬于它們,確保了它們的關系是一對一的。這最后的一行指明每個關系上的兩個元素都是相等的,確保這兩個序列是相同的,只是排序過。