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

追求神乎其技的程式設計之道系列:

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

思考的高度

上一篇談到了優秀程式設計師的第一要件:「熱情」,這一篇我想要談我覺得熱情之外最重要的能力:「思考」,特別是抽象化的思考能力。

寫程式可以說是一件進入門檻很低的工作,拜現代的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 < N; i++){
  if( !strncmp( contacts[i].name, "張", 1 ) ) // well, 讓我們假設這個strncmp支援unicode
    ret[count++] = contacts[i].number;
}
return ret;

用低階語言寫程式必須不斷處理瑣碎的細節,像是要開多大的陣列、要弄一個額外的counting變數、要用迴圈一個個檢查陣列....。當腦袋裡充滿這些細節時,是很難切換到更高的角度思考的。而functional language提供完全不同的思考方式來解決同樣的問題,以下我用Ruby的語法寫同樣的程式(Ruby具備許多functional language的特性,但不全然是個functional language):

contacts = [ { 'name' => '...', '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,但有了不同語言的概念後,思考的高度會完全不同,寫出來的程式品質自然也不同。

(待續)

34 thoughts on “追求神乎其技的程式設計之道(七)

  1. 非常欣嘗你的「教大一生 python」的作法。

    雖然我個人覺得,就一位沒有什麼,或根本沒有寫程式經驗的大一生來說,學程式設計的首要任務,除了文章開頭所說的「邏輯思考」與「抽象化能力」以外,尚需要培養出一種「喜愛或甚至熱愛寫程式的心理建設」,在沒有這層心理建設前,學什麼程式語言都是罔然,都會演變成「學校畢業後,壓根兒不想寫程式,除非了為混口飯吃」。

    但我還是想說,藉由不同的程式語言來學習不同高度的「邏輯思考」,是個非常讓人激賞的說法 🙂

  2. Drake:
    沒錯,我覺得要讓初學者喜歡上寫程式是最重要的事,接下來才是去想怎麼讓他們學得好。
    這方面我心得也不少,有空時再來分享程式教學的經驗 😀

  3. 潛水好一段時間了
    今天被一段話釣出來 :

    >沒錯,我覺得要讓初學者喜歡上寫程式是最重要的事,
    >接下來才是去想怎麼讓他們學得好。
    >這方面我心得也不少,有空時再來分享程式教學的經驗

    碰巧小弟我最近在自己系上教同學用haskell
    可是我不太知道怎樣讓聽的人”覺得有趣”
    反而一直被問”這樣有什麼用?”或是”這個有什麼價值?”

    所以..蠻期待那個”分享程式教學的經驗” 🙂

  4. 感謝vgod的文章,寫得很好。

    關於熱情的部份,我想對於大部份人而言,
    寫程式真的只是工具而已(尤其是對非本科系的人來說)
    像念經濟,念電機,他們要的只是要把他們希望得到的答案找出來,
    大學課程也可能是為了他們而設計的,所以一般都是「教了就好」,
    畢境對寫程式懷有遠大夢想的人還是不多。

  5. Vgod 學弟,

    It’s all about 熱情!

    我今年51歲, 就是因為工作壓力大, 生活索然無味, 所以想拾回兒時的夢想-學打鼓. 用 Google “Roland HD1 midi game” 找到了你, 希望很快就能如你的video一般enjoy.

    在完全意外的情況下讀了你寫程式的文章, 心中隱藏很久的熱情突然爆發, 久久不能自己. 我雖然創業并使公司上市, 目前只還是小局面, 不能說是成功. 我1973年進臺中一中, 高一時已是學校的名人, 現在想起來真的不可思議, 在那知識閉塞的年代, 無師自通, 從拆船廢電子板中取下數百個logic gates, 用手焊了上千條電線, 又自制了一排光電讀頭, 配上馬達, 把大專聯考的答案卡這頭放入,那頭吐出,分數加分扣分正確算出! 還包了模擬考來處理. 不免又上報, 又科展全國第一. 隔年做了一個電路讓示波器顯示中文, 字型ROM還是用 diode array 拼湊出來的. 科展也得大獎. 大一時自修微處理機, 并在臺大火箭社辦微處理機講習班多期, 比陳義誠老師正式在系上開微處理機還早一學期! 我記得歐陽明就曾經參加微處理機講習班,后來他被選為火箭社社長. 在Apple II 早期我迷上了基于 stack machine 的 FORTH 語言, Compiler + Interpreter + Name Space + File System 4K RAM 搞定. (當時的機器 CPU 4MHz system memory 16K DRAM!) 以 FORTH 來寫 FORTH 系統本身, 試驗 multi-tasking …. FORTH 終因不易讀而死亡, 但是 FORTH讓我幾乎學到所有Software的本質!

    講這麼多, 無非就是要分享你成長的喜悅, 在你逐漸邁向大師的路途上給你至高的祝福! 更重要的是期盼你能借鏡我不太成功的經驗: 我們在 Computer Science 習得的渾厚哲學, 雖然可以應用在其他領域, 但不是全部!! 我們應該更注意其中的差異. 這樣不但使我們不會在那些領域中失誤, 或許也可以把這些差異帶回CS領域! 舉一個例子, 假如我說生意的本質是 Random Process, 你若馬上聯想 Queuing Model 我不能說你錯, 但保證你生意做不好. 對付此 Random Process 的有效方法是培養緣分.

    什麼是緣分?! 你能解釋我們為何會在此相會, 說這些話? ….

    我在臺中一中時看了一本高商薄記的課本, 公司創業時自己寫會計程式, 結果比會計師還了解會計. 但我非常后悔沒在那時有機緣看易經! 我期望你或許能!

  6. jaiyalas:
    讓人覺得有趣的訣竅是先了解你的對象平時喜歡什麼,再把你想講的東西和他喜歡的東西連結起來。
    這樣就能很自然吸引到聽眾的注意,並讓聽眾產生興趣。

    JM:
    前輩你真是太了不起了,很高興能因緣際會在這認識你。
    希望我們都能永保熱情,一直專注在自己所愛的事情上面。

    Stanley:
    按照我目前的速度可能等我畢業都還不夠集結成書吧XD

  7. Pingback: 寫程式之道 « 凍啡走甜

  8. Vgod大您好:
    請問能否轉貼您”追求神乎其技的程式設計之道”系列文的文章連結(僅連結並列出來源,非全文轉錄)
    於p2個版和朋友們分享呢?

    對您的文章心有戚戚焉,
    我也是從gwbasic到vb寫橫向捲軸射擊遊戲,
    高中時也曾嘗試採用類Genetic Algorithm試著寫出自己產生code的程式,
    不過後來走偏跑到systems biology去了XD

  9. 寫程式是真的需要熱情
    可惜當初念書的時候, 沒有老師教過我們怎麼愛上寫程式這種東西
    多數的學生都是為了pass而考試, 為了成績而看書~ 甚至花很多的時間就只是為了做一張小抄
    一切只是為了畢業拿文憑

    這些事情在現在我這個年紀回想起來, 就覺得特別的無奈
    寫程式到現在八年多了~
    剛畢業的時候, 我還只是一個連迴圈都搞不懂得學生
    念的是二專卻念了三年~ 畢業之後出社會極盡所能的不想讓人家發現我是計算機工程畢業
    深怕人家知道一個計算機工程畢業的人居然不會寫程式…
    還好那時候遇到了一位不錯的老闆, 只告訴我一句話!!
    我給你三個月, 這三個月你甚麼都不用做, 我會給你薪水~ 三個月後我要看到你會寫程式…
    然後, 我想我現在能靠著寫程式, 不用擔心房貸..不用擔心生活, 真的要感謝那位老闆
    哈哈!!扯遠了~~~

  10. 程式人員應該要更進一步跨足計算機結構,如此視野才會開、程式才能更上層樓,不會被侷限於語言文字中。

  11. Pingback: 追求神乎其技的程式設計之道(八) | vgod’s blog

  12. Vgod長輩您好:
    在文中您提及 “邏輯思考,主要其實就是用正確、清晰的邏輯表達想法而已,說來簡單但要做好也是需要一定時間的訓練。” 想請教何謂用正確清晰的邏輯表達想法以及應該如何訓練自己這方面的能力呢?
    謝謝!

  13. Hi spanky,

    如果說要怎麼訓練邏輯,那… 寫程式最快啦 😀
    電腦只懂邏輯,如果你能練習到很順暢的把任何想法轉換成程式碼,那就達到目標了。

  14. Pingback: 網站製作學習誌 » [Web] 連結分享

  15. Pingback: 好文: 追求神乎其技的程式設計之道 | TechNow 當代科技 - web host by CommuniLink

  16. 怒贊!!!!我發現自己找到學習的目標之後,動力簡直無窮無盡,以前覺得每天學習到12點種不要太誇張哦,現在卻發現即使如此也沒辦法學完想學的東西!可惜我高中和國中時沒有學習的動力,否則………後悔也沒有啦,從現在開始珍惜每一分每一秒,踏踏實實地努力才是真理!

  17. 順著作者的UI文章鏈接跑過來的。這篇文章是不是寫的太早了?對函數式編程的理解有點小問題,所謂的Functional指的是數學意義上的Function,也就是基於集合之間映射關係的那個,這就防止了imperative programming(大陸這邊的叫法是指令式編程)中引入的各種狀態變換造成的副作用。這是因為數學意義上的函數是恒定的,也就是說如果我定義一個滿足數學定義的函數(或者叫純函數)f,那麼不管在什麽時候,對于一個指定的x調用f(x),返回值都是一樣的。這種基於純函數進行設計的語言就會非常非常的適合進行分佈式計算,因為一段代碼,只要它是純函數,就可以把它很小開銷的扔到一個運算單元上(Google著名的MapReduce就是一個採用函數式設計思想的典型例子)。至於說到函數之間可以組合來組合去,那純粹是因為大部份函數式語言爲了方便定義純函數都是基於λ-運算來設計的結果(其實就我接觸的來說是所有,但是保不齊會有人突發奇想爲了和java一樣用代碼行數來騙錢做個純粹基於圖靈機的函數式語言)。而從λ-運算的定義我們可以發現這東東並不是只能用來定義純函數,這它和圖靈機是等價的,所以其實很多新的老的語言里都開始把這個作為基本特性來使用,連c++這種史前怪獸都有在boost庫里弄出來這東東。
    不過作者在這個年紀能瞭解這麼多已經很了不起了,我高中的時候基本上就是抱著asm在和dos死磕,數據結構和算法之類的都是到了大學才開始和c++一起接觸。至於函數式語言之類的東東是工作之後爲了用好emacs而從lisp開始學習,然後發現了haskell這種優雅到了極致的語言(偶是學數學的),之後才真正瞭解到這類語言的意義。
    另外,好像MIT的計算機相關專業都要把scheme作為必修課,不知道現在是不是還是這樣。

    • Hi antiacui,

      你說的沒錯, 我了解functional programming的function是從數學的function定義的.
      沒有side effect也是function非常重要的特性之一, 只是這篇文章不需要提到這個, 我就沒有特別再加以描述了 😛

      MIT CS以前的必修是Scheme沒錯,但這兩年已經改成Python了。

    • 這正正是因為 pure functional 程式語言沒有副作用(正確地說應是 referential transparency),所以才會自然地把 function 看作數學的函數,亦即每個 function 都是把引數值轉換為傳回值(否則就沒有用了),這種「資料流轉換」的思考方式自自然然就讓 functions 變成組合來組合來去了,就像 Linux 的 command line 工具那樣了。就算把 λ calculus 換成各種 combinatory calculi 也是一種。當然要模擬 imperative programming paradigm 也行,但就不是它最自然的思考方式了。

  18. 所以學東西還是要沒有壓力最好@@
    我們快考基測了還是每天中午午休不睡覺跑去電腦教室寫程式xD

  19. 那么作为一个平凡的程序员,要怎样才能做到既能專注於自己所開發的範圍,有能对其上的架構或其下的細節都能理清頭緒呢?

  20. Pingback: 追求神乎其技的程式設計之道(十一)- 抽象化與命名 | vgod’s blog

  21. 關於”程式語言與思考的高度”那部分,
    我想問,那UML呢?是否也有能影響思考的高度?

  22. 我發現這篇文章的邏輯怪怪的,你一開始說要學習程式語言,最好不要一直被局限於低階的程序,以及低階的資料結構,所以你說你教大一Python結果你在文章中卻是拿Ruby來示範…這應該是顯示你比較喜歡Ruby吧?xD

  23. Pingback: Teach Yourself Programming in Ten Years & 追求神乎其技的程式設計之道 | CE1001 計算機概論

留言給我吧!