2008/10/12

思考的高度

程式設計和一般事務邏輯一樣,思考的高度會決定看事物的本質及真相,甚至決定未來的解決方法

轉載自:追求神乎其技的程式設計之道(七)

原文:

這次拖稿了很久,雖然下禮拜就要期中考了,但我決定還是要趁這個作業都剛交出去的忙碌低峰期來補上一篇,不然真不知道下一篇要等到什麼時候了…(泣)

思考的高度

上一篇談到了優秀程式設計師的第一要件:「熱情」,這一篇我想要談我覺得熱情之外最重要的能力:「思考」,特別是抽象化的思考能力
寫程式可以說是一件進入門檻很低的工作,拜現代的GUI開發工具以及大量的open source library所賜,很多低階、跟硬體和作業系統直接相關的細節都被隱藏起來了,所以說其實只要學會某種程式語言並且會把自己的想法鉅細靡遺的轉換為程式碼,就可以說自己會寫程式了。到達這個階段並不困難,只要有心學習的話即使是國中生自己看看書或到巨X電腦上上課都能學會。那麼究竟要如何跨過這個階段,讓自己能和巨X電腦的畢業生有所區隔呢?我認為關鍵就在思考的高度。

寫程式需要的思考能力第一是邏輯思考,主要其實就是用正確、清晰的邏輯表達想法而已,說來簡單但要做好也是需要一定時間的訓練。第二是抽象化思考,這是許多人忽略掉的一點,也是我覺得區隔一個平凡與偉大程式設計師的重要特質。

我覺得所有的程式都可以看成一個巨大的金字塔,頂端是這個程式的最終目標,一個模糊的概念;底部是細節的程式碼。而中間是一個經由不斷切割與抽象化所構成的高塔,每一個程式都是切割為許多的元件、模組,再切為更細的class和function,再來是最底下的變數與邏輯判斷式。
很有趣的是,不同的人看這個塔就會有不同的樣子。初學者看到的塔只有兩層,他們和人溝通的方法是鉅細靡遺的描述程式碼:「我在這裡寫個for,第一次把i設成0,在迴圈內每次檢查這個陣列的第i個元素…」,在他們眼中只有程式的目標和程式碼本身,所以還可能會寫出下面這種讓人哭笑不得的註解:

a = 1; // 把a設為1

有些經驗後,會再多看到一層,利用function把一段程式碼包裝起來,賦予一個名字和獨特的意義。學會這個後,就可以利用抽象化後的function名稱來溝通,例如:「我在這個迴圈裡每次都用isCaptial來檢查這個字串是不是都是大寫…」再接下去呢,可以再利用class,利用design patterns,利用更大的模組、子系統來溝通,認真說起來,這其實是一個無止境的切割。
在資訊科學這個領域,抽象化是個無窮無盡的必要行為。因為世間萬物實在太多太複雜,我們只好不斷把東西歸類,並賦予一個名稱、一個意義,經由這樣的過程我們才能用抽象的語言和符號來溝通,避免每次都要從最底層的瑣碎細節開始說起。而平凡和偉大的程式設計師,我覺得他們之間的差別就在於能看到多少這個高塔中間的分層。厲害的高手都很善於切換自己思考的高度,一下能跟你討論高階的系統架構設計,一下又能深入到最底下的組合語言和二進位除錯。他們腦中除了有這高塔每一層的詳盡平面圖,甚至也非常了解不同樓層之間的交互關係。而平凡的程式設計師大多只能專注於自己所開發的範圍,對於其上的架構或其下的細節都不一定能理清頭緒,萬一出現bug也會搞不清楚到底是哪一層出了錯,而被完全無關的細節絆住手腳。

程式語言決定了思考的高度

大部分資訊系學生接觸的第一個語言是C語言,其實我覺得到了21世紀還從C語言開始教是非常值得商議的一件事。我在台大時曾當過兩次計算機概論的助教,雖然大一學生同時還在修計算機程式設計(也就是教C語言的課),但我在課上也同時教他們學Python。
有人問我:「只學C語言不夠嗎?」。如果是為了畢業後能找工作,其實學C就夠了,因為幾乎所有公司都只考基本的C語言能力,也就是說他們認定只要會寫C就能勝任日後的工作。事實上大部分大學都不太教程式語言的,會教C也只是因為大一總得選一個語言教,而C還是老得辣,加上大部分教授也只會這個,所以自然就決定是它了。近年來因為物件導向風行,所以大部分學校還會教個Java或C++,但這也是因為要教物件導向的概念,而不是以教這個語言為目的。除了這兩種外,大概就剩下組合語言了,而這也是因為要教電腦最核心的CPU運作方式,所以才會順便教到的。
程式語言的地位在資訊系其實一直很卑微,大部分教授覺得這只是一個基本工具,就像螺絲起子和鐵鎚一樣。但我一直覺得程式語言是很重要的工具,它不只是讓人用不同語法和電腦溝通,而是讓人能用完全不同的思考方式來解決問題。簡單的說,我覺得程式語言就是決定思考高度的一個關鍵因素,而這也間接決定了寫程式的能力。
舉一個簡單的例子,高階的script語言幾乎都內建map這個資料結構。(也就是一對一的對應表,給它一個key,就能很快的找到其對應的value。有的語言稱為dictionary、hash、或associative array。)如果寫習慣Python或Ruby的人,一定會很直覺的用map來儲存任何對應關係,甚至用來表示會動態變更欄位的struct。但是,在C語言裡沒有這種東西,這讓很多只會寫C的人直覺的用陣列加上linear search來存放這種對應關係。如果資料結構學得好的人,會知道這樣寫效率很差,但很多時候因為沒有方便的library,也懶得自己寫一個高效率的map(不過是存一個電話簿,我難道要先寫一個紅黑樹嗎?),就妥協於沒效率的儲存方法。

這就是一個被程式語言限制住的典型例子。在高階語言用map存東西實在太容易了,所以這會變成思考時的一個小單位,跟人溝通或是規劃架構時都能隨時拿來用。但相反地,在低階語言裡,要有效率又簡單的儲存這種對應關係實在很麻煩,所以人們在思考時會傾向選擇容易的方法來做,而自然忽略掉了以map為基礎的解決方法。

除了script language外,functional language也是另一個進化到神乎其技路上必備的技能。functional language是以function為基礎來思考的程式語言,典型的代表是LISP、Scheme、Haskell。(這邊所說的function是higher order function,可以以其他function為參數的function,和C語言裡的function是不同的概念。)在functional的世界最棒的特性是程式可以只靠function間的相互組合而生成,不用迴圈不用if一樣可以達成同樣的目的。
舉例來說,如果我要要從一個電話簿中挑出所有姓張的人,並傳回他們的電話,用低階語言(其實我指的是imperative language,但這裡就不要這麼講究了)寫起來大概是這樣:

PhoneData contacts[N] = {…..};
String number[MAX_NUMBERS];
int count = 0;
for(int i = 0; i < contacts =" ["> ‘…’, ‘number’ => ‘…’ }, … ]
return contacts.find_all{ c c[‘name’][0,1] == ‘張’ }.map{ c c[‘number’]}

是的,你沒看錯,就只有兩行,而且真正做事的只有一行而已。這裡用到的是functional language的基本工具:filter(Ruby裡叫find_all)和map。這兩個function特別的地方在於他們能用來取代一般需要迴圈才能做的事,並賦予除了「迴圈」以外更高階的抽象意義。filter的意思是過濾,可以從一個陣列中用一個給定的function為條件來去除不合條件的元素;而map的意義是對應和轉換,可以用一個給定的function作為規則把一個陣列中的每個元素全轉換成另一個樣子。
多了這一層抽象化後,寫程式的思考方式會變得完全不同。迴圈不再只是迴圈,而是可以根據它的目的將之區分為map或filter(其實還有更多,這邊只是先舉兩個做例子),思考時便能以組合這些小元件的方式來構思程式的寫法。這裡提供的不只是語法上的簡便而已,而是整個思維的大躍進,以及思考高度的提昇

這就是為什麼我要教大一新生Python。Python融合imperative language、object-oriented language、以及functional language,語法簡單清楚威力又強大。雖然他們學過後不見得會繼續用Python,但有了不同語言的概念後,思考的高度會完全不同,寫出來的程式品質自然也不同。
(待續)

沒有留言:

張貼留言