avatarvgod's blog

总结

这篇文章是作者对于程序设计技能的追求和成长历程的分享,从小沉迷于电脑和编程,到参与各种竞赛,再到探索如何让计算机自主编程,作者不断提升自己的编程技能和思考能力。

摘要

文章开头提到作者从小对电脑和编程产生了浓厚的兴趣,通过自学和参与高中的资讯奧林匹亚竞赛等活动,逐步提升了自己的程序设计能力。作者分享了自己在学习编程过程中的经历,包括从QBASIC到Visual Basic的转变,以及在国中时期对于MUD游戏的热爱和编写机器人的经验。他强调了在比赛中的竞争经历,如何通过不断的练习和学习提高自己的解题能力,并最终成为国家队队员。

作者接着谈到了自己对于程序设计的深入思考,提出了让计算机自己编程的梦想,并尝试通过基因演算法来实现这一目标。他参与了国际科展,并通过自己的作品获得了奖项,这一经历进一步激发了他对于人工智能和程序语言结构的兴趣。作者还探讨了不同程序语言对思考方式的影响,特别是函数式编程语言如何提高抽象化思考的高度。

文章中,作者还分享了自己在高中时期如何通过“见树又见林”的学习方式,结合理论和实践来提高学习效率。他认为,程序员的熱情是成为优秀程序设计师的关键,同时强调了抽象化思维对于提高编程效率的重要性。作者还分析了不同程序语言对编程思维的影响,以及如何通过自己编写工具来提高工作效率。

最后,作者总结了自己的学习过程,强调了命名和抽象化在编程中的重要性,并提到了自己在大学期间对于科学方法和设计思维的认识。他认为,通过跨学科的学习和实践,可以发现新的问题,并提出创新的解决方案。

观点

  1. 编程能力的提升: 作者认为,通过大量的练习和参与比赛,可以逐步提高编程技能和解决问题的能力。
  2. 程序设计的内功心法: 作者强调,程序设计师需要打好基础,包括理论知识和系统底层的熟悉,以便在面对实际问题时能够快速有效地解决问题。
  3. 思考的高度: 不同的程序语言会影响程序员的思考方式,函数式编程语言能够提高程序员的抽象化思维能力。
  4. 工具的重要性: 优秀的程序设计师应该能够自主开发或修改工具,以提高自己的工作效率。
  5. 命名和抽象化: 作者认为,良好的命名和抽象化是编写出既易于理解又高效的程序的关键。
  6. 跨学科学习: 通过跨学科的学习,可以开阔视野,发现新的问题,并提出更有创意的解决方案。
  7. 学习过程的重要性: 作者分享了自己的学习过程,认为每个阶段的学习都是积累经验和能力的基石,对于未来的发展至关重要。

追求神乎其技的程式設計之道 — 2022重編版

在2022回顧「 追求神乎其技的程式設計之道」

「追求神乎其技的程式設計之道」是我從2008年開始在我的個人blog上連載的一系列文章。當初只是因為想回答一位讀者問到關於如何學習寫程式的方法和經驗才開始撰寫的,沒想到後來就一連寫了三年,共有十二篇文章。

現在已經2022了,我決定開始一個新系列的文章「軟體工程師的修煉與成長」,所以就順便把舊文章從雜草叢生的舊blog整理一下搬來Medium,讓想要回顧舊系列的讀者可以有比較一致的閱讀體驗。在這個版本中,我把十二篇文章都合併起來,做了一點刪減,也加上了一些2022的註解。

寫這系列文章的過程中,我一邊回憶高中時期的成長,一面深深感受到過去的每一個經歷都在十幾年後慢慢顯露出它的意義。就像Steve Jobs在2005年Stanford大學的畢業典禮上說的一樣,過去的故事乍看之下是毫無關係的單一事件,但到了二十幾年後的今天,這些「點」就慢慢的被連起來變成了一條很清晰的線,指引我走到了現在。而這系列的文章就在這個適當的時機,記錄下我當時最純粹的想法和心情。

從我國中時用Visual Basic寫出第一個遊戲「黑白棋」時,我就完全迷上了電腦的力量和寫程式的快感。雖然我現在是全職的軟體工程師,但變得資深的壞處是寫程式的時間越來越少。但即使如此,我還是會留時間給自己寫程式,因為我還是非常樂在其中,可以專心寫程式不用開會反而是一種休息和調劑。寫程式是一條無止境的道路,不只是科學和工程,更是一種藝術。即使這系列文章已經結束,但我還是在追求「神乎其技」的半路上。雖然還有很多要學的,但我也希望能讓初學者更容易看清楚這條路的面貌,避免陷入盲目追求新技術的死巷中。

一切的開始

如果是從DOS時代開始玩電腦的玩家,應該都知道當初DOS有兩個內建的QBASIC小遊戲:貪食蛇和猩猩丟香蕉。這兩個小遊戲是許多人兒時共同的回憶,我還記得我國小時曾有幾堂電腦課,當時老師在台上嘰哩瓜啦的不知道在教什麼,而台下每台電腦都是貪食蛇或丟香蕉的畫面。(老師對不起,其實我就是帶頭做亂的罪魁禍首…)。

微軟把這兩個QBASIC遊戲附在DOS內對我產生了莫大的影響,那是我第一次發現到原來QBASIC和不只是像PE2能打打字而已,QBASIC竟然能把一堆看起來像咒語的文字變成遊戲!幸運的是我家剛好有本第三波的QBASIC入門書,沒事我就自己拿起來翻著看,雖然當時太小,即使把整本都看完了還是搞不懂貪食蛇是怎麼寫出來的,但也誤打誤撞知道了原來這就是程式設計,原來我能直接把貪食蛇檔案內的一個數字改掉就能有幾百條命可以死,原來學寫程式就能做出電腦遊戲…。對小孩子而言,知道這些事就像告訴他魔術師袖子裡的秘密一樣,我一天到晚興奮地要老爸帶我去書局看電腦書,彷彿真的可以搞懂電腦螢幕背後的一切魔法一樣,我也夢想著有一天能寫出自己的遊戲。但當時我沒想到的是,我還真的花了十幾年的時間在探索電腦的魔法…。

MUD與黑白棋

升上國中後,家裡裝了一台28.8kbps的modem,當時的internet還沒完全成形,在沒有Google的時代internet是沒什麼價值的。當時的modem最常被我拿來上一些撥接式的BBS,那時候的撥接BBS站台還不少,最棒的是還能從站上抓到很多軟體和各式各樣的教學文章,像是如何用組合語言寫電腦病毒,如何破解大富翁2之類的文章。這些文章對當時的我就像武林密籍一樣,雖然沒辦法完全看懂,但我也是從中得到很多零碎的概念,像是16進位的換算、組合語言、中斷向量、常駐程式….。

在國二時,我還不小心迷上當時一個超熱門的MUD — 萬王之王(KK),每天放學回家都急著連上線,讓家裡電話整晚都忙線中,玩到每個月電話費都是上千元,搞得我媽數次警告要把modem收起來再也不讓我上網了。(還好她沒真的這麼做,不然我現在就沒辦法寫這篇文章了。)

MUD是現在MMORPG的純文字版,整個虛擬世界都用文字描述,並且只要用telnet就可以連上去玩了。但內行的玩家都知道,玩MUD應該要用 zMud或是UNIX下的tintin++,因為這兩個軟體可以設定所謂的trigger,偵測到某些事件的發生,就能自動採取事先指定好的動作。因為一切的訊息都是由文字呈現,所以偵測事件非常簡單,只要看看有沒有特定字串出現就可以了;而要做特定的動作也很簡單,就是送出文字指令而已。(眼尖的人一定會發現,這其實就是現在MMORPG外掛的最原始形式。)嚴格說起來,zMud是我首次寫「實用程式」的平台,我學會透過trigger在MUD的世界中 寫自動化的機器人,自動在迷宮中遊走,自動換裝備打怪練功..。這時的我突然體會到,會寫程式真是太棒了,我在MUD中簡直跟神一樣。其實當時我也不過只會用最基本的變數、if、迴圈而已,但透過在虛擬世界中寫機器人的練習,讓我的邏輯思考概念有飛快的進步,也給我了非常強烈的動力想好好學一個正統的程式語言。

升上國三後,很幸運的透過推薦甄試提早上了台中一中,升學壓力解除後,老師和父母就完全不管我要幹麻了。這時我終於有了一段完整的時間可以好好的再把BASIC重新學過,無奈的是在我國三時QBASIC已經快滅絕了,取而代之的是Windows上的Visual Basic,我只好硬著頭皮買本新書來從頭學起VB。當時我看的是王國榮的VB 5入門書,整本書有六七百頁吧,比我國三所有課本疊起來都還厚,現在想想小時候真的有點不知天高地厚竟然相信自己能看完這麼厚的磚頭書。那時候我每天上課就帶著這本磚頭去學校,這樣看了幾個禮拜下來,沒想到我這時突然都看得懂了,很多原本不知道有什麼用途的概念突然都相互連結起來了。(多虧了在MUD裡的訓練!)就這樣,某天突然有種打通任督二脈的感覺,我發現我全搞懂了,迴圈、陣列、Windows GUI控制項、去背貼圖…,我突然想通要怎麼用程式語言寫出遊戲了。

從那之後,每天回家就是打開VB寫程式,我想寫個黑白棋來檢驗自己的想法,我把自己知道的所有概念都放進去,有GUI元件、有貼圖、有動畫、有音效..,這是我第一個完整的程式,從頭到尾每一行都是自己寫出來的。(以現在的眼光來說只能說是一個期末project規模的小程式,但對當時的我可是意義非凡)

這個黑白棋讓我印象最深的其實是debug的痛苦經驗。我花了一個禮拜把程式的核心部分完成,但在吃子的時候卻跑出一個不明的bug會打亂整個盤面。為了找這個bug,我又花了一個禮拜,每天從早到晚都在想哪裡寫錯了,後來慢慢trace了好久,才發現竟然只是一個變數忘了歸零!!!

這種bug很常見,不過只是programmer最容易犯的無心之過之一,但這件事對我的影響非常大,它讓我花了很長時間在想以後要怎麼避免犯同樣的錯。我後來才知道一個普通的programmer和厲害的programmer從這裡就會分出高下:普通programmer犯了這種錯會覺得很平常,並提醒自己下次別再這麼笨了,但實際上不久後一定又會再犯同樣的錯;厲害的programmer會反省自己寫程式的方法,並改變原有的方法或習慣來避免以後再度產生同樣的bug。

古老的程式設計教材(尤其是C語言),都說要把變數宣告在函式的一開頭,並且因為變數宣告完還得經過初始化,所以很多人習慣是在函式開頭宣告並初始化所有變數。這不是錯的,可是,這其實就是會導致bug的元兇。

因為變數在開頭就被初始化,這樣在真正要用到它的時候就能直接拿來用,但是如果這個變數需要被歸零(也就是重新初始化)並在迴圈中重複利用,就很容易會忘記要再多做這一步。(在多層迴圈中更容易發生)

我為這個bug苦惱了幾天,後來才意識到這是coding style的問題,只要改變宣告變數的習慣,就能避免犯這種錯誤。如果一開始就在迴圈內宣告並給定變數的初始值,而不是在函式開頭宣告,就不會有這種bug跑出來了。有了這個經驗後,我歸納出一個原則:「永遠在變數需要被用到的最內層區塊才宣告並初始化該變數。」這種原則很重要,我日後一直放在心裡, 它也幫助我避免掉未來再犯同樣錯誤的可能。(事實上,我後來再寫了二十年的程式,再也沒有比這更痛苦更長久的debug經驗了…)

資訊奧林匹亞與程式競賽

在升上高中前,我因緣際會透過一個國中的同班同學認識了他的哥哥YJL。YJL比我大三年,我要進中一中時他剛好畢業,很巧的是他很會寫程式,一直都是中一中的資訊能力競賽代表隊成員。剛認識他時,他就demo給我看他用QBASIC自己寫的橫向捲軸射擊遊戲,當時看得我目瞪口呆,我完全沒法想像 QBASIC竟然能寫出這麼順暢且華麗的遊戲。這個demo如果不說,我一定會以為這是市面上在賣的商業遊戲(我那時還以為一定要學C才能寫出這種遊戲)。

透過YJL我也得知原來高中還有資訊能力競賽和奧林匹亞這種比賽,聽他述說跟全國的高手一起比賽寫程式時,讓我不知不覺也熱血沸騰起來。我把他高中時留下的各種參考資料全帶回家,並透過他認識了更多還在中一中的強者學長們,就這樣在踏進高中校門的同時也決定了我這三年的方向。

這裡我先介紹一下對於高中生最重要的資訊比賽。高中的學科能力競賽是教育部主辦的比賽,包括數學、物理、化學、生物、地球科學、資訊,每間高中通常會先辦個校內初賽來選出代表選手,再由這些選手參加各區域的能力競賽,最後各區的前N名(每科的人數不同)才能參加全國競賽。以資訊科來說,我記得是校內取6名,中區再取6名進全國決賽。到了全國決賽能拿到前10名,還能直接保送進資訊奧林匹亞的培訓營,不用再另外參加培訓營的入營考。至於資訊奧林匹亞 (International Olympiad in Informatics, IOI)則是國際性的資訊比賽,參加的人是從每個國家挑選出來的頂尖高中生,每年輪流由一個參賽國舉辦,選手要進行兩天每次連續五個小時的頭腦比賽,其中只有一半的人可以得到獎牌。

這些比賽和一般的程式比賽其實很不一樣。這種比賽比的是解決問題的能力,不是比賽軟體實做的能力。題目比較像數學問題,只是除了紙筆外,還得用程式語言實做出能解出正確答案的程式,也就是說參賽者必須想出問題的解法(演算法),再透過自己擅長的程式語言寫進電腦,讓電腦執行後輸出問題的答案。這種解題比賽主要考驗的是運用資料結構和演算法以有效率的方法解決問題,並寫出正確程式的能力。大學有個類似的比賽是ACM ICPC,問題類型和IOI很類似,但比賽的模式和方法則有很大差異,有興趣的人可以自行尋找相關資料。雖然IOI是給高中生參加的比賽,但IOI題目的水準其實非常高,如果你能輕易解出IOI的問題,那… 我跟你打賭去Google面試也有99%的機率會通過。(*) 順便一提,Google Code Jam就是一個開放給所有人參加的解題比賽,題目類型就跟IOI和ACM ICPC差不多,前一百名就有獎金,還能免費去Google Mountain View總部玩一玩喔。

參加比賽是一個評估自己實力的好方法,沒在比賽會場上較勁過,真的很難體會解題與寫程式能力的差距可以有多麼巨大。一個頂尖的程式設計師和一個普通的程式設計師,其生產力是很輕易的能有十倍甚至百倍以上的差距,而寫出來的程式碼品質及效率也是同樣會有如此巨大的落差。

透過良性的競爭,高中這段時間也成了我進步最快的一個時期…..

(*) 2022註:現在矽谷的軟體公司面試幾乎都是用類似IOI和ACM ICP題目的方式在篩選工程師,甚至有一個有名的網站 LeetCode 搜集了很多面試題目和提供線上的練習平台。但如果有比較過這些題目難度的人就會知道,其實高中生和大學生的比賽題目比LeetCode難多了…。

勁敵

熱血的少年漫畫都有一種標準公式,熱血但什麼都不懂的主角,加上一個天才勁敵的刺激,讓主角能在不斷遭遇的困難和挫折中不斷爬起來進步。就像火影忍者中的鳴人和左助,或是棋靈王中的進藤光與塔矢亮,要進步最快的方法就是找到一個遠遠超過自己的勁敵作為目標並且努力打敗他。

我在高一時參加校內的資訊能力競賽初賽,在都還搞不太清楚要比賽什麼東西時就去參加了。當時只會用BASIC,知道要用筆寫程式時還嚇了一跳,還懷疑閱卷老師難道能在頭腦裡執行程式嗎?我記得那題目不難,我每題都有寫,但最後只拿了個佳作,離學校的代表隊還遠得很(話雖如此,我也是唯一有得獎的高一生了)。過了幾個月,我非常驚訝的發現台北市的資訊能力競賽,竟然有一個建中的高一生SBB拿了一等獎,更可怕的是他接下來又在全國能力競賽拿到二等獎,才高一就已經有能選上奧林匹亞國手的氣勢,前途無可限量。

受了他的刺激,我非常拼命學習,先花一個月自己把C語言學起來,接下來就到圖書館借回所有有關資料結構和演算法的書,每天都拼命看。這是我進步最快的時期,當時我在家自己跟著已經進入IOI培訓營的選手們一起練習ACM Online Judge上的題目。每天到學校都在看Introduction to Algorithms, 因為沒有電腦,只好用紙筆練習解ACM Online Judge上的問題。白天在學校想解法,回家就在電腦上把程式寫出來,並submit上去看看結果對不對。透過這種方式練習,可以看到自己解出問題的數量不斷增加,強烈的成就感能刺激自己不斷練習不斷思考,如此循環之下真的進步得非常快。

很快地,在我升上高二後,我發現我已經寫了三百多題,進入ACM Online Judge世界排行榜的前幾名了。同時我也發現有另一個跟我一樣每天都增加好幾題的人,沒想到竟然就是跟我同年的建中SBB。雖然我每天都能寫兩三題,但SBB的解題數仍遠遠在我前面,逼得我只好印出所有題目,帶去學校不管上課下課都在紙上解題。就這樣我們的差距慢慢拉進,不知不覺我們也站上排行榜的前兩名。如果我今天多寫一題,我就能暫時站上第一,但隔天馬上又會被他超越過去,彷彿他早就已經囤積了數十題起來等著慢慢折磨我一樣。

就在這樣的刺激下,我就像漫畫中的熱血主角一樣快速成長,進入了勁敵所在的境界。我順利從校內初賽、中區預賽,一路拿第一進到全國決賽。非常戲劇性的是,在這年全國能力競賽,SBB拿了滿分得到第一,我錯了半題排名第二,而全國能力競賽一向是前兩名都為一等獎,也就是說我和SBB原本應該都是一等獎,沒想到評審說我和第一名的滿分有個落差,所以一等獎就改成一名,而我就變成了二等獎第一…(實在很嘔,但也不能怎樣Q_Q)。比賽雖然輸了,但我沒有很難過,因為我知道自己還能再進步,還能變得更強,繼續努力下去我很可能可以選上奧林匹亞的國手。

國手之路

高中各學科的奧林匹亞是每年一度的國際盛事,奧林匹克運動會比的是人類體能的極限,而各學科的奧林匹亞比的則是運用腦力的極限。我第一次是從YJL那聽到這比賽名字的,但那時覺得非常遙遠,因為我看到連他這麼厲害的人都選不上國手,可見這不是一般人能輕易踏進去的領域。

改變我想法的是中一中穿堂的一張照片。中一中有個不錯的傳統,只要代表學校參加比賽或科展獲獎,學校就會把獲獎學生的照片掛在穿堂的榮譽榜上。還記得高一時在穿堂閒晃,一個個欣賞歷屆學長們偉大的功績,突然間發現有個叫CLK的學長厲害得不得了,不但在中區能力競賽拿第一,到了全國也還是第一,而且他還選上了97年IOI的國手。後來我才知道他以前也是中一中電研社的社長,而那屆的副社長也非常厲害,自己寫了一個microkernel作業系統代表台灣去美國參加國際科展。有這些如傳奇一般的學長,給了我很大的激勵作用,我這時突然覺得自己或許也能像他們一樣有照片被掛在上面的一天。現在想起來,還真不知道當時自己怎麼能這麼有信心,其實有種不知天高地厚的感覺。但或許也因為如此,才沒有被這種「看起來很困難」的目標嚇到而連嘗試的勇氣都不敢拿出來。

很有趣的是,在我剛上高一時,CLK是中一中第一個也是唯一一個資訊國手。但就在我高一下的時候,有三個高三的學長竟然同時選上99年IOI的國手。因為一年資訊國手名額只有四個,一直以來都是建中學生呼聲最高,這年我們一口氣拿下三個名額簡直是不可思議的奇蹟。我也因此而信心大增,彷彿我每天在他們旁邊練習也能受到逸出的強者氣息感染而變強一樣。

就在我在全國能力競賽拿到第二名後,我就開始為IOI培訓營做準備。IOI培訓營大約收30人,要關在師大內集訓四週,每天都請教授來上課,每週進行一次模擬考。第二個禮拜結束會先淘汰一次,留下來10個人再繼續廝殺,直到四個禮拜結束。培訓營過後不是馬上就能知道成績,還要過一段時間才會知道四個國手是誰,但因為留到第二階段基本上就等於有了保送任何大學資訊系的資格,所以第二階段的氣氛其實還比較歡樂一些。

在師大集訓的四週讓我認識很多在這個領域頂尖的強者們,我覺得這更勝於實際上在課堂上學到的東西。這些強者們如今都還持續在資訊界的各個領域活躍,常常覺得不管到哪參加活動或研討會都還是可以碰到這些有相同背景的人,可見這個培訓營真的也訓練出許多資訊界的中堅份子。

在集訓時,大家也不是每天各自悶著頭寫程式。其實很不好意思說,當時在培訓營進步最多的可能是魔法氣泡對戰的戰術…。雖然有電腦就會被拿來玩電動,但培訓營的學生還是比較特別。每年培訓營都會流行一些能用程式寫AI來決鬥的遊戲,像是坦克大戰、俄羅斯方塊等等,我們那年流行五子棋,大家利用空閒時間寫五子棋的AI,碰在一起時就讓各自的程式互相決鬥分個高下,玩起來比自己親自下去玩還刺激很多。

經過四個禮拜的集訓,每天討論演算法,每天寫程式,到最後連睡覺都會夢到程式碼。當時因為才高二,也不會覺得壓力很大一定要留到第二階段甚至要選上國手,一直抱持平常心反而意外表現得不錯。結訓過後,我每天沒事就盯著培訓營的網頁看,希望能趕快看到國手名單公佈。就在名單公佈的那天,我盯著螢幕驚訝的說不出話,我竟然真的選上國手了!這真的是平常完全不敢想像的事情,真的沒想到我竟然也有一天能讓自己的照片被掛在中一中的穿堂上….。

程式設計到底是什麼?

2000年IOI在北京舉辦,這年台灣的代表隊成績還算不錯,拿到三銀一銅,比較可惜的是我第一天表現不理想而落到了銅牌,雖然不至於兩手空空無顏面對江東父老,但也知道自己的實力大概就在銀牌和銅牌的邊緣處吧。IOI結束後,我又回到了學校,但因為已經取得大學保送資格,在學校其實也是輕鬆寫意,成天就看自己的書或研究自己有興趣的東西。

在這段時間中,我開始有所警覺,我發現我雖然很會寫程式解題,但那都是一兩百行以內的小程式,真實世界的程式根本不是這個樣子的!雖然我能很快看出一個問題該用什麼演算法效率最高,並且在很短的時間內把自己的想法正確地轉換成程式碼,但我還是不知道市面上的軟體或遊戲是怎麼做出來的。

我這時才開始接觸C++和物件導向的概念,我突然發現要寫個大程式還真不是簡單的事,除了程式語言外,還有好多瑣碎的函式庫得學。像是要畫圖就要學2D的SDL或是3D的OpenGL,要做Windows GUI程式就要學Windows SDK或是MFC,要寫網路連線就得學socket,要讓遊戲執行順暢甚至得用組合語言寫某些部分…。好多好多東西不斷湧出來,學這些東西很有趣,因為我一邊學就會一邊聯想到學會這個功能後可以用在遊戲裡的什麼地方,於是整個學習過程就像把我夢想中的拼圖一塊一塊拼上去一樣,非常有成就感。

邊寫這種實用性的程式時,我也發現以往在比賽中累積了很多不好的習慣,像是濫用全域變數、變數隨便命名、把整個程式塞在main裡…。這些壞習慣在寫小程式看不出來有什麼差別,但隨著程式規模變大,這就變成了很致命的習慣。而這種習慣一但養成,之後會變得更難改,所以強烈建議初學程式設計的朋友們,一開始就不要偷懶,從認真幫變數想個好名字開始吧!

這段期間也讓我想了很多關於程式設計的有趣問題,像是寫程式到底算是科學+工程,還是藝術?寫程式必須要非常非常精確,任何一個字打錯都可能會讓整個程式跑出完全不同的結果,這對於天生就容易犯錯的人類來說實在是艱鉅的挑戰。為了避免錯誤太多,我們只能用一些固定的流程並強迫程式設計師遵守,讓可能的錯誤減到最低,這就是所謂的軟體工程。雖然有工程的影子,但寫程式卻是很難精確管理的工作,因為面對同樣的問題,不同的人絕對會寫出不同的程式,甚至是提出不同的解決方法﹔有的程式可能要跑三天三夜,有的程式卻能在瞬間得到正確解答﹔有的程式碼雜亂不堪,也有的程式碼井然有序清晰易讀﹔有的人要花三天寫1000行,也有人能在一天寫100行就達到完全相同的效果﹔這些程式的目的可能完全相同,但呈現方法卻有千萬種,軟體工程難道可以限制每個程式設計師大腦運作的方式和速度嗎?

從程式碼的觀點來看,不同的人寫出的程式碼也一定不相同。從程式碼的排版、命名、段落安排、抽象化程度、運作流程可以看出作者的個性、態度、思考邏輯及深度。從這個角度來看,寫程式更像是種藝術,就像是畫筆或樂器一樣是一種表達自我並將思想具體化的工具。

另外我很感興趣的是,人一定要寫程式才能叫電腦做這麼多複雜的工作嗎?能不能教電腦寫程式,讓人只要告訴電腦要寫什麼樣的程式就好?或是有沒有更簡單更方便的方法能和電腦溝通,並且保有同樣的控制力?

就在被這個問題困擾著的同時,我意外從一本書看到基因演算法(Genetic Algorithms)這個名詞。稍微研究過後讓我大吃一驚,因為我發現基因演算法是一個超級有效率的搜尋演算法,可以在幾近無限廣大的可能解裡面很快找到接近最佳解的答案。所以,我很快想到了,如果想要讓電腦寫程式,其實就是告訴他要寫的程式要達到什麼目的,並讓他在幾近無限大的可能程式中找出能跑出我們需要答案的那個程式。這是一種把寫程式視為搜尋的概念,我當時想到這件事非常興奮,但我並不知道其實早就有人想出同樣的概念(這叫Genetic Programming),並已經做了許多研究。

其實有時候無知是件好事,這樣才會有勇氣在不知道這個問題有多難的情況下去嘗試看看。如果我當初就知道這問題其實是能拿好幾個博士學位甚至是得到圖靈獎(Turing Award, 資訊界的諾貝爾獎)的難題,我可能連繼續嘗試的勇氣都不會有了。

讓電腦自己寫程式的夢

在高三突然對於讓電腦自己寫程式產生興趣後,我每天就都想著這件事,夢想著未來某一天的電腦能跟我一起寫程式,當我把程式目的說給它聽時,它就一邊把細部的程式碼產生出來:我不用想如何命名變數,或是要用什麼演算法,還是我到底要不要把這個功能變成獨立的class…;我只要動動嘴巴:「我想寫一個橫向捲軸的射擊遊戲。玩家操縱著會變形的飛機,還有四種武器。第一關要長這樣這樣……」,然後電腦就幫我把遊戲寫出來了!如果真的可以這樣,那該有多好啊!(如果真的成真,恐怕我也要失業了?)[註一]

這個想法實在太讓人興奮了,雖然說這是一個遙遠的夢想(其實那時候我並沒有覺得這麼遙遠,只能說自己太不自量力XD),但也開啟我對於人工智慧、程式語言結構、軟體工程等領域的高度興趣。

寫程式很難?

在高中時代頗不知天高地厚的我,一邊嘗試著利用基因演算法(Genetic Algorithms)讓電腦自己產生程式碼,一邊研究各種程式語言的結構和特性。靠直覺摸索出利用tree作為中介來描述一個程式後(那時沒唸過compiler,不知道這個其實就是Abstract Syntax Tree),我突然就對這個想法的可行性信心大增,所以一時衝動就去報名了國際科展,打算把這個想法實做出來。

真的做了以後才發現,最難的部份不是產生出程式碼。如果你把程式語言的基本元素,像是if、for、變數、運算符號等東西做成一塊塊磁體,然後拿給一隻猴子玩,那麼牠其實也能拼出一堆程式給你,問題是:「你要怎麼知道這些程式碼真的達到了你想要的目的?」

悟出這個道理後,我突然了解寫程式最難的部份是在驗證程式碼真的跟你所想表達的事情完全相同。(所以說我對於各家軟體公司的QA地位都低於RD其實感到很不平)

基本上,我們只能設計大量的可能輸入值丟進程式裡,並比對程式跑出來的結果和我們想要的輸出相不相同。即使在這種情況下,我們也只能說這個程式在測試過的這些輸入值上所產生的結果是正確的。也就是說,除非我們測試過所有可能的輸入(這意味著無限多種可能),不然永遠沒辦法知道某個程式是對還是錯。除此之外,即使只測試一個輸入值,也還有個很嚴重的問題:「我們怎麼知道這個程式要跑多久?」換句話說,當你程式跑下去,你怎麼知道他是掉入一個無窮迴圈,還是其實正在拼命計算當中?

我在參加國際科展時,認識了歐陽明教授,他告訴我這個問題叫做 Halting problem,Alan Turing在70年前就證明了這是一個無解的問題。知道這件事後,才發現自己所知實在太少,對於資訊科學的基本知識實在非常不足,但這次經驗其實也給了我一個明確的方向,讓我把書中的理論和實際的目標連結在一起。(*)

(*) 2022註:當初我研究的這個問題,DeepMind在2022年釋出的AlphaCode用現代的Transformers神經網路把這個領域又往前推了一大步。現在他們產生出的程式已經可以達到Codeforces上面人類平均的水準了。

見樹又見林的學習之道

提到書本中的理論,有很多人問過我要怎麼學習寫程式或資訊相關的知識,我順便在這邊分享些心得給大家參考。

常看到許多人抱怨大學裡學的東西都是理論,畢業後找工作時才發現什麼都不會都得重學;當然企業也會抱怨,大學應該多教一些實務課程,不然出社會後還得重新訓練。說來說去,一致的口徑指向理論和實務是打死也扯不上關係的樣子,尤其念資工的人更常這麼說:「學校為什麼不教C#?為什麼不教我做網頁?好歹也要教個HTML嘛!」

我覺得學習任何事物,一定要有充分的興趣才會有效率,在不知道所學為何的情況下被逼著學習是非常痛苦且沒有效率的。最近看了一本書:「沒有資優班,珍視每個孩子的芬蘭教育」,書中提到芬蘭教育成功的秘密在於「見樹又見林」,這句話也是我對於如何學習最想分享的秘訣。

台灣的教育方法是「先見樹,再見林」,也就是先教你細部的方法和技術,等你都學會之後(或是硬背起來之後),出社會後就會知道為什麼要學這些東西。(很多人小時候都聽過「等你長大就知道唸書有多重要」吧。可是現在比較多人畢業後反而說「我不知道之前念那麼多書有什麼用」)在這種體制之下,許多人在還沒見到整片森林的美景前就被一棵棵大樹搞得暈頭轉向,痛苦萬分,在不知道「學了這個可以做什麼」的情況下,不管學什麼都會覺得沒有意義沒有動力。

而「見樹又見林」的學習方式,是先找到能引起自己興趣的目標,讓自己有個理由去認真學習,之後再往細部的技術和理論去學習。我從小就很想自己寫遊戲,為了達成這個目標,我就四處尋找相關的資料,慢慢的我就知道自己應該要學好一個快速的低階語言(像是C++),如果要寫繪圖引擎可能還得學一點圖學的理論和技術,如果要做網路連線還得學網路相關的技術…。

這裡有個重點是,不要看過森林後就忘記它,而又迷失在幾棵樹幹上,要讓自己一直重複見樹又見林的過程。

這跟如何有效開發軟體的秘訣是一樣的。一個有效率的軟體開發方式是用 iterative process,把包含設計、實做、測試的iteration時間縮短,但要一直不斷重複這個iteration無數次來改進現有的成果。同理,在已經有目標的狀況下學習時,每當學會一些新東西,就要馬上試著把它實現出來,即使是只有幾行程式碼的prototype也沒關係。只要一直不斷的學,這個prototype就會 一直被改進,一直加進最新學到的知識和技術而更接近最終的目標。

同時動腦也動手(註二),用這種角度來學習就能充分了解自己學會的東西可以用在什麼地方,馬上得到回饋的成就感還會刺激自己繼續向前進,形成一個非常有效率的學習循環。用這種角度看學校教的東西,就能知道書本上的理論可以用在哪,並且又欠缺哪些實務知識讓自己無法做出想要的東西。

這方法理論上可以擴展到任何事物的學習,重點在找到有興趣的目標和書本裡知識的連結,我覺得這是老師應該要出力的地方,無奈的是台灣的教育體制把中小學老師們變成出考題和改考卷的機器人….。

(註一) 其實,現在真的有這樣的研究,而且驚人的是目前已經有了非常接近我想像中未來的雛型。最讓我興奮的是MIT Media Lab的Hugo LiuHenry Lieberman做的 Metafor:只要對電腦用英文描述你要的程式,電腦就自動把Python code生出來給你….。

(註二) 話說, MIT的校徽上就是一個拿著鎚子的工匠和一個拿著書的學者,所代表的意義就是 “Mind and Hand”,也就是期望每個學生都能手腦並用,除了做夢外也要實做出來才算數啊。

熱情

最近看到兩篇不錯的文章 程式設計的兩個觀點 (1/2)程式設計的兩個觀點 (2/2),指出程式員的兩種型態。一是重視演算法、資料結構、執行效率的「效率魔人」,二是重視程式架構、擴充性、彈性、可理解性的「架構狂」。這兩種人其實都很好,要完成一個偉大的軟體,團隊中兩種人一定都要有。比較糟糕的是,有很多「第三型態人」,他們的信念只有一條:「程式只要會動就好」。第三型態人不在乎效率,也不管架構漂不漂亮,上面要求他做什麼,他就想辦法東湊西湊,從Google找程式剪貼,從MSDN抓範例來用,反正只要能隨便測過一個case就能交差了。

其實第三型態人也不一定是不懂演算法、不懂design patterns,他們常常只是因為火燒屁股了,就不管三七二十一先弄出可以動的程式再說,效率或架構等到下一階段再來改就好…。問題是,下一階段又有新的功能要做,這些人再度面臨抉擇時還是會決定先讓程式「會動再說」。我看過很多各式各樣的程式員,只要碰到這種人,同樣的過程是履試不爽不斷出現。

所以要成為一個優秀的程式設計師的關鍵是什麼?關鍵不在於coding速度有多快、懂多少演算法,或是背了多少patterns,最重要的是「熱情」!

偉大的程式設計師都非常喜歡寫程式,寫程式的過程是一種絕妙的享受,他們執著的地方或許不同,可能是程式的效率,也可能是開發的效率,甚至是架構的彈性或是程式碼的精簡美觀程度,但他們都非常想要並堅持自己應該寫出「好程式」。熱情能驅動他們把軟體的某一個面向雕琢到極致,這需要超乎常人的毅力和堅持,以及絕不向壓力妥協的精神。只要具備這種熱情,不管你在乎的是什麼,都可以成為一名偉大的程式設計大師。

思考的高度

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

寫程式可以說是一件進入門檻很低的工作,拜現代的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為基礎的解決方法。

除了scripting language外,functional language也是另一個進化到神乎其技路上必備的技能。functional language是以function為基礎來思考的程式語言,典型的代表是LISP、Scheme、Haskell。(這邊所說的function是 higher order function,可以以其他function為參數的function,和C語言裡的function是不同的概念。)在functional的世界最棒的特性是程式可以只靠function間的相互組合而生成,不用迴圈不用if一樣可以達成同樣的目的。

舉例來說,如果我要要從一個電話簿中挑出所有姓張的人,並傳回他們的電話,用比較低階的語言寫起來大概是這樣:

PhoneData contacts[N] = {…..};
String number[MAX_NUMBERS];
int count = 0;
for(int i = 0; i < N; i++){
  if(!strncmp(contacts[i].name, "張", 1)) {
     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,但有了不同語言的概念後,思考的高度會完全不同,寫出來的程式品質自然也不同。

Intel ISEF國際科展

在之前提到我一直以來都夢想著讓電腦學會自己寫程式。就在我把這個想法的雛型用基因演算法實做出來後,沒想到竟然真的看到了一點結果,電腦真的能透過基因演算法來產生出一個能夠正確運作的程式!

在高三時,已經取得保送資格的我對學校的課業時在沒什麼興趣,於是我想閒著也是閒著,乾脆就把這個點子拿去參加科展看看,這樣一來我就又有理由可以請公假了(笑)。當時似乎是因為已經來不及報名全國科展了,所以我就跑去報名了國際科展,但那時我其實完全不知道這兩種科展有什麼差別。後來查了一下才知道,原來台灣的國際科展除了是一種比賽外,主要的目的其實是要選出代表去參加其他國家(如美國、法國、加拿大…)的科展活動;而一般的全國科展就是一個區域至全國性的展覽兼比賽,在全國選出前幾名後就結束了。

2001年時,那時科教館還在建中對面,那也是我第一次踏進科教館的大門。在展場內把自己的海報貼上看板後,我就到處逛逛欣賞別人的作品。因為資訊科在高中不是正式科目,參加的人比起其他科來說少了許多,但其中還是有些非常驚人的作品出現。當時有個建中的學弟Eric,他寫了一個用類神經網路辨識並動態追蹤影像中人眼位置的程式,於是他就用了個普通的攝影機加上這程式,就變成可以用眼睛控制滑鼠游標的神奇裝置。

除了Eric的作品外,其他的作品看起來大多只是某種現有產品或課本上習題的複製品,並沒有什麼令人特別印象深刻的東西出現。說起來這其實也不奇怪,因為在高中階段要自己學好一個程式語言其實並不容易,在基礎還未打穩前,即使有再多創意也無法發揮出來。而很可惜的是,很多人上了大學終於學會寫程式後,創意和熱情也被磨損的差不多了;而畢業後雖然已經有了基本功力,但卻只能照著老闆開的規格刻畫死板的功能與介面,與其說是程式設計師不如說是程式工匠。

我覺得台灣人的能力並不差,但社會中卻瀰漫著一股抄襲與仿冒的氣息,從最近的酪梨壽司事件(相關抄襲事件還可參考MMDays的整理),可以看出抄襲風氣在台灣並不只是小時候在學校抄抄作業而已;媒體工作者在報導中任意抄襲及轉載是種不尊重自己專業的表現,不但隱含著一種應付了事的心態,更代表著這些人對於自己的工作沒有熱情,更沒有著一點堅持。如果要說我在美國看到這邊和台灣有什麼最大的不同,我想關鍵的差異就在對自己的工作有沒有熱情和堅持而已了。

在評審的過程中,有兩個教授一起來聽我介紹我的作品。因為我沒有任何參加科展的經驗,也沒做什麼講稿或準備就去了,一切只能靠臨場發揮,還好教授們還蠻喜歡我的作品,一來一往的討論之下才讓我不至於太緊張而說不出話來。

自己的介紹結束後,終於鬆了一口氣,教授們對我的作品似乎感到非常新奇和有興趣,後來還問了我「如果再給你兩個月,你能做出更好的結果嗎?」這問題聽起來像是要給我個機會再繼續深入研究下去,難道是意味著要選我當代表嗎?當時我對於得獎其實是沒什麼興趣和期望的,只是想看看別人對這個東西的意見,但如果有人喜歡當然是很棒的事情。這問題無非是給我挑戰的機會,我腦中還有很多改進的想法,如果有更多時間,一定還能做出更棒的結果。所以,我就毫不猶豫的就回答了:「可以,當然可以!」

到了頒獎典禮時,司儀一一宣佈每個學科的獲獎人以及之後要代表台灣去哪個國家參展,同時也頒發一些企業贊助的特別獎。我原本一直以為Eric的作品應該是穩拿第一的,畢竟相較之下我的作品並不夠成熟和完整,甚至連個能稱上科學實驗後的結果都沒有。但這個新奇的點子威力還是很大,我完全沒預料到無心插柳參加科展竟然讓我拿到了Intel頒發的電腦科學最佳獎,並且還選上了美國代表,之後可以代表台灣去美國參加Intel主辦的國際科學暨工程展 (ISEF, International Science and Engineering Fair)。

程式設計之內功心法

得獎當然很開心,這是對於我這個想法和努力的肯定,但隨之而來的也是一股很大的壓力。我得代表台灣站出去,到美國跟來自世界各地的教授、學者、參展代表用英文介紹我的作品,那不能像在台灣這樣輕輕鬆鬆跟教授聊聊天就好,我要用不熟悉的語言跟不熟悉的外國人介紹我這個只花一兩個月做出來的雛型作品…光想到這個背都涼了。

選上科展代表跟選上奧林匹亞國手是完全不同的感覺。奧林匹亞對我來說是個已經努力很久的目標,我很清楚自己的實力有多少,我對自己花了將近一年所打下的基礎很有信心,而且我對比賽本身已經非常了解,出國比賽不過就是做我已經很擅長的事情而已,一點都不需要害怕和擔心;但選上科展代表真的完全是意料之外的事情,我自覺還沒有花足夠的功夫在這問題上研究,就像一個腳底有油漆還到處亂跑的小鬼,糊里糊塗的鬼畫符被長輩當成畢卡索的作品一樣。

驚恐之餘,我還是警覺到我得開始做很多事來讓我能充滿信心出國去參展。我得趕快把英文練好,至少要能流利的跟人介紹作品,還要能回答各種問題;我還得了解Intel ISEF到底在幹麼,參展代表要做些什麼事;最重要的是還得繼續在這個作品上做更多研究,看能不能在兩個月內做出更好的結果。

參加科展讓我意外領悟到一些事,我發現我高一開始花了一年多投入資訊比賽所得到的並不只是那塊獎牌與一堆獎狀,在練習的過程中,我把台中圖書館所能借到的演算法和資料結構的書全都看過了,就連冼鏡光當初在微電腦傳真雜誌上的專欄也被我從圖書館地下室的陳年庫藏中挖出來,一本一本的影印裝訂起來。我收集了我所能找到的所有大大小小資訊比賽歷年來的題目,加上當時ACM Online Judge上做過的四百多題,我當時參加比賽幾乎都是看完題目馬上就能想完所有可行的演算法和所搭配的資料結構,並用直覺挑出能最快寫出來且又最有效率的解法。除了解決問題的方法外,我也能輕易的把任何想法寫成程式碼,只要能把過程講出來,就能毫不猶豫寫出code來。

這個花了一年多練出來的功夫,讓我到今天都受用無窮。這種感覺就像唸完九陽真經後內力大幅提昇,之後不管再練什麼武功都是易如反掌。在高中時,我改以Linux作為我主要的工作環境,同時也幫中一中架起BBS,並自己架了web server、mail server來玩玩。這些系統底層的功夫乍看之下跟寫程式沒什麼關係,但我後來發現要做各種能在現實生活中應用的軟體系統,總是會碰到這些現有系統的細節問題。

我以前在大學時常觀察別人的project會碰到什麼阻礙,而開發不順利的原因幾乎都是被一些瑣碎的細節所絆住而導致嚴重的進度落後。這些細節都是些小事,甚至跟寫程式沒有直接相關,但總是會讓人陷於泥沼之中。舉一個簡單的例子,假設要在Linux上寫程式需要用到某個library,動手前得先安裝一下。這個看似簡單的安裝其實牽扯到許多瑣碎的細節,像是:distro有沒有提供package可以直接透過網路安裝? 沒有的話就要自己編譯,那就得知道如何用configure、make等工具;編譯中可能還會發生缺少其他相依的工具或package的情形,這時還要能從錯誤訊息中看出到底少了什麼東西,並且想辦法先安裝起來…。這些事情跟寫程式的理念其實一點關係都沒有,但很殘酷的是,有許多人都會被這種細節所絆倒而中途放棄。

紮實的內功和熟悉系統底層的基本武功帶給我非常有效率的實做能力,我可以在想出新的idea的同時馬上勾勒出實做上大大小小的細節;我可以很快設計出核心的高效率演算法,也知道系統各部份的功能有什麼現有的library或系統可以利用,這兩種能力讓我能快速完成prototype。我只怕沒有夠好的點子,完全不會擔心是不是真的能做得出來。

不久前有位數字先生開了一個程式速成班,號稱能在4堂課內教會web程式的開發,只要帶著點子過去就能在上完課後開始創業。我覺得學寫程式本身其實不用很久,我也曾教過一個12歲的小朋友寫程式,兩個禮拜總共約十小時就足夠讓他掌握基本的程式邏輯概念,包括變數、迴圈、if、陣列、抽象化..。但認真說起來這些東西只是基礎內功,有了內功後自己還是要花很多時間去學相關的底層武功,像是要做web程式還得摸熟web server的架設和管理、UINX的shell和系統管理、各種相關網路傳輸協定、資料庫的使用和管理、前端的HTML和CSS設計….。光web程式所牽扯到的實做細節就多如牛毛,如果要在完全沒有穩固基礎的情況下同時學這麼多東西,只要一旦出現問題,一個對系統從上到下每個環節都不熟悉的人是完全沒辦法鎖定問題發生點的。而debug的基本概念就是要先鎖定問題發生的地方,要做到這件事的先決條件就是要很有信心的先排除一些不可能的地方,再做些假設並驗證假設是否成立來判斷可能問題。如果對每個環節都沒有充分的經驗,我不覺得這樣的人有能力清楚的定位出問題所在,更別提是否能獨力完成一個像樣的完整系統。

每個人都聽過成功是一分的天才加上九十九分的努力,如果說天才是能想出絕妙idea的能力,那我覺得還有個關鍵是,要在有點子前先做過夠多的努力打好基礎,等到靈光一現時才能把握住機會馬上實現它。如果等到一分的天才出現時,才準備開始做九十九分的努力,那很容易就會被許多基礎能力不足所帶來的挫折感和障礙所壓垮。

高中到大學

春天的國際科展結束後,我的高中生活也即將劃下句點。回顧高中三年,從校內的資訊比賽一路走到奧林匹亞的國際舞台,在保送大學資格的保護下,到高三又誤打誤撞變成國際科展的代表。雖然從結果看來一切都是如此美好,但在我剛升上高中時,沒有人知道未來會發生什麼事。我爸媽只想要我好好唸書,考上好大學;而我對學校教的科目早已失去興趣,每天都只想泡在電腦前探索這個神秘盒子的內部。於是每次考試結束,家裡就會因為我的爛成績而引發一次大戰,我爸媽也會一再的限制我坐在電腦前的時間。

就像七龍珠裡的標準情節一樣,悟空總是可以和敵人奮戰到垂死邊緣才爆發出最後一擊取得勝利,我總覺得在逆境時才能看出一個人真正的意志力和求生能力。雖然我爸媽能不斷縮減我能用電腦的時間,但他們可沒辦法限制我在學校做什麼或是面對課本時心裡到底在想些什麼。我還記得高一時,我天天都帶著 Introduction To Algorithms(當時還是第一版,很多人叫它白皮書)到學校,不管上課時老師講得多麼口沫橫飛或是下課同學們玩得多開心,我都是看我自己的書。有時碰上管得嚴的老師,我就不看書開始想ACM的題目,用筆在白紙上寫code。我還記得當時為了手邊要隨時有充裕的題目可以想,我把ACM online judge上所有的題目全都印了出來放在一個資料夾裡隨時帶在身邊。

現在想想我那時瘋狂的程度真是難以想像,我當時心思幾乎完全沒放在學校課業上,就連體育課我都躲在樹蔭下想題目的解法。於是到了要升高二選類組時,家裡又爆發了第N次的世界大戰。

還記得升高二前有個考資優班的機會,我爸媽非常想要我去考考看,但我死都不答應。我很堅持的原因是,中一中的數理資優班非常奇怪,進去後就自動變成第三類組,也就是要多念一科生物,但我已經很確定未來我只想念資訊相關的科系,所以我只想選二類,不想浪費時間多念一科生物。就為了這件事,爭執的戰火延伸到我花太多時間在電腦上未來會考不上好學校之類的陳腔濫調上。

台灣父母似乎都想要孩子選擇他們覺得的安全、穩定的路,但我大概天生叛逆了點,說什麼都只想堅持自己的路。不管成功的可能性有多小,或是風險有多大,我覺得我就是應該照著心中的聲音去走。跟他們吵了一陣子後,我決定跟他們立下一個約定:讓我完全自由到高二,如果我沒辦法在資訊比賽拿到夠好的成績足以保送大學,那我到聯考前就都不碰電腦,即使我考不上大學也是我自己的決定所造成的。

從此以後,我就更認真的拼命研究演算法,研究歷年來所有大大小小資訊比賽的題目,只要我醒著的時間,腦袋就全速運轉放在這些東西上。雖然理論上我應該是背水一戰,但那時心裡其實沒有太大壓力,反而覺得可以每天都在玩自己有興趣的東西很幸福很開心,如果比賽結果不好,那可能只是說明我沒有這個天份和這個命而已。

現在回想起來,我覺得當時的我真是不怕死到了極點。要靠資訊比賽保送,至少也要進入奧林匹亞的培訓營,並成為留到第二階段的最後十人之一才有機會(有機會的意思是大學有可能會拒收…)。簡單的說就是實力大概要在全國高中生的前十名就是了。

我想我當時一定沒有想這麼多,只傻傻的做自己想做的事,所以才會這麼有勇氣立下聯考前都不碰電腦的可怕約定。還好我運氣很好,上了高二後從第一場校內賽就把我累積一年的力量爆發出來,就這樣一路打進培訓營甚至還選上國手。

順利拿到保送資格後,父母鬆了一口氣,於是就完全放任我花更多時間泡在電腦裡了。到高三時,我誤打誤撞說要參加國際科展,於是就有了正當理由請公假,各科老師對我的缺席也見怪不怪。高三我幾乎每天都待在教官室裡玩Linux、架各種系統和server來玩,雖說要做科展,但其實也是對什麼有興趣就玩什麼。(可能很多人覺得奇怪,為什麼會待在教官室… 因為教官室是少數有電腦有網路,我又能自由進出的地方。再加上當時有教官找我幫忙做網站,所以….)

到了要畢業的時候,我才發現我高三幾乎都沒踏進教室過,班上的同學我也幾乎都不認識。雖然在自己的世界裡過得很開心,但要畢業時才發現我的高中生活和其他人都不一樣。班上老師們對我也頗有微詞,所以畢業前我還碰到了一個可怕的畢業危機:我高三的學科被當了九科……。(現在想起來還真難想像,到底有哪九科啊orz…)

這件事大概是我一帆風順的高中生活中最驚悚的一件事了。雖然台大已經張開手等我進去,但要是高中不能畢業,那我不就變成比櫻木花道還悲慘的笑話了嗎…。(註:櫻木花道在全國大賽前發現被當太多科無法參賽,所以只好閉關唸書準備補考,當然最後還是順利過關去參賽,不然灌籃高手就畫不下去了。)漫畫中的主角當然不會被要補考這種小事打倒,但現實生活中誰知道呢… orz

在畢業前遭受到如此巨大的危機,實在完全出乎我的意料之外。還好當時帶我們參加比賽的指導老師非常挺我,幫我跟各科老師要一個補考的機會。學校也很好心不打算找我麻煩,就讓我把考卷帶回家寫一天。隔天交出去後我就沒再得知過關於成績的事,我甚至不知道我畢業時每一科的成績,但總之我還是拿到畢業證書了(汗)。

回想起年輕時的瘋狂,真是覺得很不可思議。如果我當時沒有堅持自己的想法,而依照爸媽的意思「好好唸書」,我想現在的我一定過著完全不同的人生,至少不太可能憑聯考進入台大資訊系,而現在也不會在MIT了..。

程式設計師的生產力之謎

很多人都聽說過,同樣是寫程式,一個頂尖程式設計師和一個普通程式設計師之間的生產力可以有十倍甚至百倍的差距。這是其他行業很少見到的現象,於是不禁令人納悶,為什麼會這樣呢?

其實單就解決同一個問題的coding能力來說,差別還不至於這麼巨大。我在高中參加奧林匹亞和大學參加ACM比賽時,已經遇過全世界數一數二的coding高手,這些人每天都在練習解題和實做各種複雜的演算法和資料結構,隨便說一個演算法都能直接把code透過肌肉記憶反射出來給你。他們能在數十分鐘內想出高效率的演算法並寫成程式解決一個複雜的難題,即使我高中時已經花了一整年在訓練,依然不是他們的對手。這些人除了絕頂聰明外,每個人在高中時少說也寫過至少十萬行的code。但我發現,即使他們能解一般人解不出來的問題,打字快到鍵盤會冒煙,在做一個大系統時,這方面的差距還不一定會如此明顯。

所以我想,除了先天的智商和後天對於寫程式的熟悉度與經驗外,一定還有什麼因素造成巨大的生產力差異。

我後來發現,程式設計師和其他行業有個很大的不同點:一般行業只能在現有的工具上磨練自身的技術,但程式設計師除了磨練技術外,還可以獨自創造、修改自己使用的工具;換句話說,程式設計師的能力就是在電腦上創作出更好的軟體,不但能便利他人,也同時能增進自身使用電腦的工作效率。舉例來說,理髮師能磨練使用剪刀和設計髮型的技術,但理髮師並不知道怎麼發明及製造新的剪刀讓自己更有效率的剪頭髮;電機、化工、土木工程師要設計IC、化學製程、建築結構,但他們得依賴電腦軟體才能設計,並且靠許多大型機器和工具才能生產,即使想提昇自身的工作效率,也不是自己一個人想做就能辦到的。但軟體工程師就不同了,我們只靠一台電腦就能工作,我們的工具是軟體,我們的產出也是軟體,我們的所依賴的一切都是軟體,只要自己願意投入心力,隨時可以修改每天使用的工具和系統讓自己更有效率的工作。

這一點可以說是程式設計師的先天優勢,也是頂尖的程式設計師和普通程式設計師的生產力差距的關鍵。

Eat Our Own Dog Food

我發現厲害的程式設計師常有種共同特質:寫工具給自己用,解決自己日常工作碰到的問題或改善自己的工作效率。英文有句話叫eat one’s own dog food,字面上意思是說一家公司應該要吃自己做出來的狗食,實際上是引申為一家公司應該要在內部用自己的產品解決自身的問題,才能發現真正的問題,並且說服人這東西真的很實用。Google是奉行「吃狗食」原則的典型公司,Google內部有一個獨立的搜尋引擎搜尋公司內部網路的資料,新技術也都能在上面先行測試;Google內有獨立的gmail、google docs,都在對外公開前先讓內部的人實際用上好一陣子;Google甚至做了自己的Linux distribution給工程師用、自己的compiler、自己的瀏覽器…。

會這樣做的動機其實很簡單:如果我每天都要花10分鐘手動做同樣的事情無數次,為什麼不花一個小時寫個程式把這件事情自動化,以後可以省下更多時間做點真正有意義的事;如果我每天都要用這個彆腳的軟體,忍受他設計上的不便,為什麼不想法辦改善它或自己做一個呢?

程式設計師每天都用電腦在工作,尤其做一個系統時常常需要花時間做一些瑣碎的工作,像是編輯設定檔、把一些檔案搬來搬去、重開伺服器、清除暫存檔…等等。這些工作通常不難,但可能步驟繁雜,或是每個步驟都要等待它慢慢完成,累積起來每天就很容易浪費許多時間手動做這些無趣的事情上。

出於「懶惰」的美德,頂尖的程式設計師工作時想的不只有產出最終產品就好,而是如何花最少力氣最少時間把產品做到最好。但這件事說來容易做來難,能不能實行往往跟程式設計師工作的系統環境有很大關係。

UNIX是Programmer的天堂

如果要說我高中時期最大的收穫是什麼,我想有兩件事的重要程度不相上下:一是我確認了自己對寫程式的熱情,並且花了足夠多時間打好基礎;二是我在這段時間內摸熟了Linux,並且愛上了UNIX世界工作的哲學。

UNIX可以說是一個非常適合程式設計師工作的天堂,UNIX的工作哲學(泛指所有UNIX like的系統,像是Linux、BSD、Mac OS X..等等)是提供許多小工具,每樣小工具只做一件事,使用者可以合併使用多種工具完成複雜的工作。此外,UNIX的工具都是以command line為介面,非常適合寫script做自動化的操作。而在Windows的世界中則完全不同,Windows上的軟體傾向於提供整合式的GUI環境,把所有相關或可能會用到的功能全都一手包下,雖然方便使用者,可以點幾個按鈕就自動做完所有事情,但對於程式設計師來說其實不是一件好事。

以寫程式的工具來說,在UNIX下可以選自己喜歡的文字編輯器,不管是Vim或Emacs,也可以選自己喜歡的compiler,可以用 makefile加上command line工具自動化整個程式編譯、測試、執行的流程,並且隨時都可以加入shell script或是perl script把開發流程中瑣碎的工作一併自動處理,不管需求有多麼特別,這種完全自訂的流程彈性可以說是無限大。

但在Windows環境,主流開發軟體以整合開發環境(IDE)為主,像Visual studio這種無所不包的巨獸,對於新手來說按一個鍵就能搞定一切,不用去想底下的細節,實在方便得很。可是依賴於IDE卻有個致命缺點:只要是IDE的設計者一開始沒考慮到的事,你就沒辦法做。

舉例來說,早期visual studio不支援版本控制系統(version control system)時,程式設計師就很難把版本控制整合到開發過程中(例如說在編譯成功時自動commit、或是自動加入新的原始碼…);即使到現在 visual studio已經支援了一些常見的版本控制系統,用它的人依然要看這個支援的清單包含哪些系統,如果是想用新的系統,或是用非標準的方法使用這些系統,恐怕只能遺憾的兩手一攤什麼事也不能做。有些IDE雖然有提供plugin功能(像是Eclipse),但其實也沒好到哪,因為IDE龐大無比,寫 plugin的難度和所需時間遠高於使用者手動完成這些日常瑣事所花的力氣,自然不會有多少人願意去做。

簡單來說,用IDE的話,程式設計師沒辦法掌控自己用的工具,也沒辦法改善開發流程中的問題,開發效率的極限很自然會被IDE限制住。但UNIX上 的環境就不一樣了,採用多種工具的組合讓自己能掌控開發流程中每個步驟。因為每個步驟和所用的工具都是透明的,只要有需求或是發現哪裡可以改善,很容易可以從滿手的工具中抓出一個來解決,或是寫個迷你的script來自動處理較複雜的工作。即使哪天有新的小工具出現(例如說一個新的版本控制系統),因為每個小工具都是獨立運作,靠程式設計師自行整合的,所以也比較容易把舊的「零件」替換掉。除此之外,open source興起後,帶有原始碼的系統更帶給程式設計師一個「完全控制」的機會,除了能替換零件外,有能力的人還能直接對零件本身做修改以符合自己的需求。種種因素加起來,讓程式設計師不再被單一個工具能力所限制,而是手中握有無數可以自由運用的籌碼,唯一的限制只有自身的創意和熱情而已。

狗食是生產力的關鍵

我一直覺得念資訊系的人不應該依賴於用IDE寫程式,當然這並不只是因為「用command line才是真男人」這種geeky的理念,而是念資訊系的應該要能擅長於「把電腦能做的事交給電腦做」。今日的軟體介面大多是以多數的日常使用者為目標,雖然方便操作,但設計者不可能把所有使用者可能會想做的工作列表做出各種排列組合放在選單裡。如果是設計者沒考慮到的功能,使用者往往也只能手動慢慢處理。但程式設計師和一般使用者是不同的,程式設計師的能力就是和電腦溝通,讓電腦用我們想要的方式幫我們完成工作。如果我每天要重複按照順序按100個 不同的按鈕,為什麼不寫個程式自動按這100個按鈕?

我跟一些人聊過這個想法,但典型的反應是「正事都做不完了,哪裡有時間先做一個工具?」

但我覺得很諷刺的是,正是因為抱持這種觀念才會讓生產力低落,所以讓正事做不完,時間越緊迫就越不敢花時間做這種沒有立即產出的事情。相反地,如果在意識到自己已經三番兩次手動重複執行同樣的冗長工作時,就應該靜下來好好想想是不是有什麼辦法可以讓電腦來做這些事,只要常有這種想法,寫這些script和小工具所節省下來的時間和自己得到的經驗是一輩子都用得上的。

現在有許多open source軟體一開始都只是程式設計師為了方便自己所寫的小工具,在做正事的時候撥出時間先做好工具,然後不知不覺可能就成了偉大的軟體。最有名的例子 是Knuth為了寫他的The Art Of Computer Programming,他竟然先重頭自己打造一個針對數學環境設計的排版系統,最後就成了著名的TeX。他不但完成了電腦科學界的聖經,還「順便」完成 了一個經典的排版系統並分享給全世界使用。

如果持續抱持著這種態度寫程式,說不定你我也能成為下一個Knuth呢? (笑)

簡潔、彈性、效率

我一直覺得寫程式是一種藝術活動。程式語言是一種要求極度精確的表達方法,只要少打一個字母就可能造成完全不同的結果,但同時卻又不限制你要如何達到目標。

程式設計師有極大的自由來讓一個程式按照自己的想法「活起來」,不同人針對同樣的目標所寫的程式也一定不同。有人會用極簡主義來把變數命名為a、b、c,也有人會把用匈牙利命名法讓變數前後長出鬍子和尾巴;有人堅守DRY原則(Don’t repeat yourself),只要類似的程式出現兩次,就把他們抽象化成一個函數,也有人用copy/paste寫程式,不管怎麼page up或page down都一直看到一樣的東西還能泰然自若;有人寫程式把所有東西都塞在main裡面,也有人寫個Hello world就要搞一個class HelloWorld(雖然有些時候是被囉嗦的J語言強迫的…);有人沒聽過Big O也寫程式寫得很開心,但也有人嫌stdlib的qsort太慢硬是要自己重寫一個…。

儘管每個人的信仰和原則不同,但大體上程式藝術家也不過是在「簡潔」、「彈性」、「效率」這三大目標上進行一連串的取捨(trade-off)和最佳化。

「簡潔」的程式也「易讀」,沒有多餘的敘述或重複的程式碼,每個概念都只有唯一的一段碼在描述它。如果多了,就容易產生不一致的行為,如果少了,就是沒做到該做的事。有「彈性」的程式容易修改和擴充,只要在一個對的地方彈彈手指,不用因為老闆朝三暮四或是需求改變就得把整個程式重新翻修一次。有「效率」的程式會用最適合的資料結構存放每一樣資料,用最快的演算法做每一項必要的計算,並去除任何不必要的間接行為(indirection)。

雖然目標很明確,但程式設計之所以像藝術就是因為大部分時候我們都沒辦法兼顧這三項目標:為了效率,可能就得犧牲彈性和簡潔;反過來說,為了彈性或簡潔,也常得犧牲效率作為代價。幸運的是,效率的追求在電腦硬體和編譯器技術的進步下已經不像20年前那麼重要,只要選對資料結構和演算法,幾乎已經沒有必要手動做低階的最佳化。除去效率之外,彈性和簡潔其實是比較容易同時達到而又不互相衝突的目標。要達到這目標,其中關鍵的能力就是今天的主題:「抽象化」(abstraction)。

最簡單但也是最難的事情

很多人沒聽過抽象化這個詞,甚至以為自己不會這件事,但其實從我們宣告第一個變數起,抽象化就已經開始了。

「這個變數要叫什麼名字?」

幫變數命名時,其實就是在賦予那個變數一個「意義」。人的記憶力有限,很難記住大量且沒有意義的資訊。但如果資訊有了一個固定且有邏輯的名字,我們也就有一個容易記憶的符號來代替整個複雜的概念。換句話說,我們可以把非常複雜的概念濃縮為一個容易處理和記憶的小單位,這個過程就叫做「抽象化」。

抽象化可以讓程式變得簡潔。好的程式設計師會習慣從重複的程式碼中找出共同或相似的部份,並且把這個部分提取出來變成一個更通用的概念。任何複雜的概念都可以被抽取出來替換成一個變數、一個函數、一個類別、一個模式、一個模組、甚至是一個系統,並加上適當的命名,就能讓這個程式「一看就懂」,任何註解都不需要寫。抽象化也能讓程式有彈性。經過適當抽象化的程式,每個概念都有一個獨立的「單位」(可能是變數、函數、類別、模組、或系統)可以表示,每個概念中包含的細節也被隱藏在適當的範圍內,不管要修改或擴充原本的程式都能讓需要碰觸的地方減到最少。

雖然抽象化是讓程式簡潔又有彈性的關鍵,但出乎意料的這是一個容易理解卻很難精通的能力。抽象化做得太少,程式會變得凌亂不堪,不同層級的概念和資訊互相交雜在一起,不僅讓程式變得難讀也難改。抽象化做得太多,就是所謂的over design,明明需求只有印一個Hello World,卻用了10種design patterns蓋起101大樓以應付根本就不會出現的「未來需求」。

抽象化這個主題可以講三天三夜講不完,但今天我只想提其中最簡單也最難的事:「命名」。

命名可以說是寫程式時最簡單但也是最難的事了。這件事沒什麼人會教,沒多少書會寫,因為這件事看起來非常容易,即使你把程式裡的變數照字母順序a, b, c, d, e, …命名也是行得通,反正對編譯器來說變數或函數的名字不過就是一個沒有意義的符號,不管你取什麼名字最終都只是對應到一個像是0×08048374這個樣子的記憶體位置而已。

簡單來說,一個變數是叫「小狗」或是「小貓」,對電腦來說都沒有區別,但對人來說,差別可大了。

很多初學者以為程式是寫給電腦看的,只要看起來好像能跑出正確結果就好,所以變數位置隨便亂放、名字也隨便亂取、每個變數都是public、甚至一個函數有幾百行,為了在一個畫面中塞下更多程式碼還把IDE的字型縮小到要瞇著眼才看得見。也有很多人覺得高手寫的程式看不懂是正常的,等到自己等級提昇後應該就會看得懂了,但其實事實完全不是這樣。我認識的每個高手和大師寫的程式碼都是乾淨、簡單、易懂,即使是極端複雜的演算法,都能直接從程式碼中看懂作者的想法。

Martin Fowler的 “Refactoring — Improving The Design of Existing Code” 一書中有一句話我很喜歡。

Any fool can write code that a computer can understand. Good programmers write code that humans can understand. (任何一個傻瓜都能寫出計算機可以理解的程式碼。唯有寫出人類容易理解的程式碼, 才是優秀的程式員。)

一段好的程式碼是不需要任何額外註解或說明的。如果名字都取得好,每個變數就能適當的解釋了自己的角色,每個函式都說明了自己的功能,整個程式讀起來就會像在讀說明文件一樣自然。在這種境界下,只要有了基本背景知識的程式員應該都要能輕易地看懂。英文中有個詞叫做explain itself很適合用在這,也就是自己應該要能完美的解釋自己的一切,不需要其他的人或文件來幫忙。

但是,命名是很難的一件事,可以說是寫程式中最接近「藝術」的一部分了。我說的命名,不是要用大小寫混雜的「CamelCase」或是底線分隔的「underscore_separated_style」這種風格問題,而是一個方形到底要叫rectangle或是x的差別。名字取得好,不但自己或其他人未來再回來看這份程式碼時容易進入狀況,對於正在開發中的程式也可以減少很多不必要的bug。

我之前當一門課的助教時,有個作業是要學生實作一個西洋棋遊戲,畫面上要有個棋盤,還有該有的棋子。既然是個棋盤,底層很自然的就會用個二維陣列來表示棋盤的狀態,例如說我們會有

Chess board[N][N]

這樣子的一個陣列。接下來,真正的問題來了,程式中勢必會有一些兩層的for迴圈去對這個陣列做操作,如果是你會把這兩個迴圈的index變數取做什麼名字?

最常見也最不用腦的index命名就是i和j,在一般沒有特殊意義的迴圈中用i是沒什麼太大問題的,因為大家都知道這只是一個單純的index。但如果用到j,通常就代表程式可能有些臭味了,至於會用到k、l、m… 那這個程式一定是徹底腐敗了。

我看了很多學生的程式,我發現很多有bug的程式都是用i、j,或是x、y來命名,而那些寫得很漂亮的程式,幾乎都是用row和column來命名(或是他們的縮寫r和c,或是row和col)。

用i、j的問題在哪?

問題在這兩個名字沒有和棋盤的位置有直接關連,看程式的人沒辦法一眼看出你的i到底是指row還是指column,或是指到宇宙裡的一顆星星。即使是正在寫程式的作者本人,也得一直在心中做i是row、j是column的轉換,但只要精神稍不集中,或是吃個飯休息回來,很輕易就會忘記這些隱晦(implicit)的對應關係。而這種隱晦的對應,就是傷害程式碼可讀性和造成bug的通緝要犯之一。有的人為了避免自己忘記這些細節,就會把這種隱晦的關係或假設寫在程式的註解裡。但話說回來,既然要寫,直接寫在程式碼裡不是更好嗎?

除了用i、j的這群人外,還有另外一群用x、y的程式也是讓人非常頭痛,如果要我比較的話,我會說用xy比用ij還糟糕。為什麼?因為這個程式最終要把棋盤畫在螢幕上,而所有2D繪圖的函式庫都是用x、y來表示螢幕上的位置,如果棋盤用xy,螢幕繪圖也用xy,這樣如何分辨這個xy是棋盤的位置還是螢幕的位置?用xy這群人的解決方法都大同小異,比較懶惰的就是用x1、x2,甚至是x和xx;好一點的會用boardx和screenx,但以index變數來說還是太長太囉嗦了。

與其費這麼大力氣區分兩種xy,如果一開始就用完全不同的名字來存取棋盤和螢幕,不就沒事了?以二維陣列來說,用row和column符合natural mapping,不用再心中自己多做一次轉換。此外,現代程式語言的多維陣列大多是row-major排列,也就是說A[r]就能取到第r個row,A[r][c]就能取到第r個row的第c個元素;但如果用xy來存取二維陣列,就要把xy反過來,寫成A[y][x]才能取到第y個row的第x個column。結果就是在這個程式中很多用xy的人都把row和column順序搞反,導致初始化的盤面整個轉了90度。

我以前參加程式比賽時,看過很多經過長期訓練的選手因為比賽的時間壓力而養成不好的習慣,像是把所有程式碼寫在main裡面,變數不是aa就是bb這種沒意義的名字。在程式比賽這種特殊的環境裡,每個程式的目的就是解一個有明確輸出入規定的問題,加上有時間限制,所以選手們都是盡量用最短的code來實作自己的想法。這種情況下寫的程式可以說是用完就丟,只要比賽一結束這個程式的生命也就到了盡頭,所以很多人就不會去思考命名的問題。

到大學的時候,我也常幫同學在作業deadline前夕看他們的程式幫忙debug。很奇妙的是,大學課程的期末專題或是作業應該都有充裕的時間可以慢慢「設計」一個程式,但很多人都是在最後一兩天才開始動手,於是在作業死線的壓力下也沒心情去好好設計一個程式的架構,更別提要好好想每一個變數的命名和位置,也就浪費了許多可以好好練習這個命名藝術的機會。

命名和抽象化是一體兩面的事情。當你能把一個概念用一個適當的名稱來稱呼它時,你才有辦法把這個概念當成一個基石往上建構更複雜的事物。在此同時,人們也才能用這些簡單的名稱來討論複雜的概念或想法。如果你在寫程式時常常沒辦法用很簡單的話跟別人解釋你的程式,通常也代表你的程式是一團漿糊,沒有條理和層次。在這種情況下,你怎麼知道漿糊裡是不是黏了一堆臭蟲呢?反過來說,當你能用簡單清晰的白話跟人解釋你的程式時,你也一定能把程式寫得一樣乾淨漂亮有條理。

如果你現在還在用a, b, c這種變數寫程式,不妨先暫停一下,好好想想每個變數的意義是什麼,你的程式就會自然的變得越來越簡潔和漂亮。

番外篇 我的學習過程

還記得小時候我和我弟上過一個奇特的數學補習班,叫做「功文數學」。他們的「教學」方法非常特別,不像一般的教室會有一個老師在講台上教課,而是每個人會拿到一疊數學題目,每一面都有數個計算問題,像是「10 x 2 = ?」這樣的問題。每次去功文的任務就是把那一疊題目寫完,寫得越快的人就可以越早回家。那些題目說來沒什麼意思,從頭到尾全都是計算問題,難度從最基本的加減乘除,一直到微積分都有。每次寫完題目後,會有個類似老師的角色,拿出解答本幫你對答案,打上成績。如果在那老師能應付的範圍內,他可能還會跟你說說哪裡犯錯了,下次多加油之類的,但如果到了國高中程度的問題時,老師的作用就只剩對答案而已了。

我之所以說這個補習班很奇妙,原因就在於這個教學過程中,講求的是大量的計算和自我學習的能力。我從加減乘除開始,一路這樣寫到了微積分,寫到後來那邊的老師也沒辦法批改了,乾脆把解答本拿給我們看,讓我們自己看解答學。起初我還覺得這個補習班挺不錯的,只要我做得越快就能越早回家,對小孩子來說是個很好的誘因。而且因為大量的訓練,讓我的計算能力變得非常好,國小國中時的數學考試我都不用準備也能很快寫完交卷。但後來我開始覺得不太對勁,很多題目我雖然知道怎麼算,但我其實不懂為什麼要這麼算,或是這麼算的意義是什麼。可是台灣的考試也不管這個,反正你只要能得到答案,根本沒人在意過程是怎樣。

功文數學的這套方法對訓練計算能力而言很有幫助,因為計算就是需要大量的練習才能變快變好。但問題是,計算能力再好,也是只能算已經定義清楚的問題(也就是考卷上的問題),而無法發現新問題並定義問題,更糟的是還會讓人習慣在沒有完全理解背後的概念時只學得方法快速得到答案。

在我高中開始參加程式比賽後,我用了我熟悉的這套方法訓練自己的程式能力,自己一個人大量的找題目練習。每天從早練到晚,即使沒有人教我也覺得很正常,因為我從小就是這樣自學起來的。這樣練了一年後,我能飛快的寫程式和解題,看到熟悉的題型就能馬上開始敲鍵盤,但同時我也開始覺得寫程式變成一種機械化的過程:看題,解題,寫程式,看題,解題,寫程式…。跟我從小做數學似乎沒兩樣。

我開始覺得無趣。即使我程式寫得再快再好,也只是解出一個別人設計好的問題而已。

心中的聲音不斷的說:「為什麼我要做別人早就知道答案的問題?」

我想要做沒有人做過的事,看到沒有人看到的問題,再親手解決這些問題。

當我開始這麼想的時候,我正在一邊學著做科展。科展全名是科學展覽,意味著要做點跟科學有關的事來展覽。以前的文章提過,我高三時夢想著要讓電腦自己寫程式,我覺得這是一件沒人做過的事,非常酷,所以我就一直專注在這件事上(其實當時就有很多人在做這種研究了,只是我不知道而已)。但從一開始到我被選上去美國參加國際科展時,我都一直不覺得這件事科學在哪,頂多說是一個工程(或工藝)作品。但選上代表後是有些好處的,台大的歐陽明教授給了我一些指引,讓我知道「科學方法」得測量和量化這個作品的結果,才有客觀的數據可以知道它的作用,進而跟其他相似作品比較好壞,也因此我才開始對「做研究」這件事有點概念。(到這時我才想起來,小時候雖然有做一些物理化學實驗,但從沒人跟我說過這些實驗是有一套標準的方法和流程的,當然也沒人跟我說這件事的價值所在。)

雖然做科展挺有趣的,但同時我也對科學感到失望 — 因為我意識到科學方法沒辦法幫助我們發明新東西,只能用來評估和檢驗已知事物的好壞。至於所謂的創新科學研究,也是得先提出一個假設(hypothesis),然後再用這套標準方法去驗證它是否成立。但到底要假設什麼事情呢?科學可沒辦法告訴你。

上大學後,我看到當時處在黃金時代的MIT Media Lab所產出的許多創新研究成果,也開始接觸到設計(design)這個領域。我發現Media Lab的人大多有跨領域的背景,像是設計師+電機工程師、或是音樂家+軟體工程師,他們常看到別人看不到的問題,並提出簡單又優雅的解決方法。我以前一直以為做設計的人講求的是美術天份,但後來深入了解後才發現這是個大誤會。設計的目的是解決問題,跟程式設計師其實沒兩樣,只是用的工具是紙筆或模型罷了。(但一個好的設計通常也都很「美」就是了)

雖然如此,設計師和工程師也有很大的不同。設計師對四周環境和日常生活很敏感,常常得在生活中注意各種細節的不完美之處。但工程師成天泡在電腦中,而且很善於使用其實很難用的介面和程式(像Linux、vi、command line、還有各種程式語言),甚至引以為傲,日子久了也就不覺得這些東西有什麼問題。泡在程式碼中的工程師也一樣,如果習慣了和前人或其他人的大便碼相處,久了也就不覺得臭了。

發現這些現象後,我開始學著跳脫出原本習慣的一切,開始注意生活周遭的各種細節,思考為什麼這個東西當初要這樣設計、這樣做有什麼好處和壞處、有沒有更好的方式之類的問題。之後,當我習慣觀察細節後,慢慢察覺到我習慣用的軟體、工具、環境、程式語言,處處都是設計後的結果,而且充滿可以改進和創新的空間。這些東西都不是沒來由的產物,而是經過某些人思考過後的結果,甚至是經過好幾輪的演化結果。可惜平常在學習或教學的時候,很少人會提到這些歷史淵源和演化過程,以至於這些設計都變成理所當然的存在。但如果我們能仔細觀察平常的事物,進一步思考就會發現很多設計都是為了因應當初時空環境的限制,而這些限制現在不一定存在了,所以我們就會有發揮的空間。

眼光拉遠後,能看到的問題更多了。到了這個階段,能力強的人會覺得能解決的問題也很多。但上天給每個人的時間是一樣多的,這時重要的事情反而又變成:「找出最重要、最根本的問題來解決,而不要被眾多的小問題和小機會所分心,才能產生最大的影響力」。

回顧我的學習過程,我會覺得每一個階段都是一塊基石,一塊塊往上疊以後才會具備該有的能力和經驗做下一階段的事情。舉例來說,要是我一開始沒投入程式比賽的練習累積足夠的實作能力,之後我就沒辦法隨心所欲的寫出我想寫的程式,也沒辦法參加科展體會做沒人做過的事有多麼有趣。之後我可能就會一昧沉浸在鑽研各種流行技術中,或是眼高手低說得一嘴好主意但卻做不出什麼來。

雖然學習是一步一步往上走的,但過程中每件事都有反面的效應,讓我不知道是不是做別的選擇會更好。像是我覺得功文數學浪費了我太多時間在數學計算上,而限制了我在其他方面的發展,但同時它也讓我養成靠自己學習的習慣;參加程式競賽也有類似的效應,雖然增強了我的程式能力,但也讓我錯過正常的高中生活和課程(雖然說到目前為止沒有覺得有什麼負面影響)。

無論如何,我相信在成長的過程中適當的大量練習是必要的。異數(Outliers: The Story of Success)一書的作者Gladwell說要精通一件事情至少需要一萬小時的練習,我相信這是真的。我在高中為了比賽所做的練習起碼就超過五千小時,上大學後輕易就超過一萬小時,但其實我也不覺得我真的精通了什麼。經過大量的練習,本來很難的技巧或技術都會變成一種本能,可以很自然的使用它來從事更高階的應用或是建構更複雜的技術。沒有這些基礎,也就很難站在更高的地方看得更遠想得更多,我也不會走在現在的道路上。

Programming
神乎其技的程式設計之道
Recommended from ReadMedium