2016年9月30日 星期五

如何整理學術文獻

從大學到博士班,一路上花費在文獻閱讀上的時間也不算少。有時回頭看會覺得有些方法或概念如果當初能夠早點知道,或許就不會走了那麼多冤路,那些浪費的時間就可以用來從事其他休閒活動,例如讀更多的文獻。因此我希望把過去這數年閱讀文獻的一些過程技巧記錄下來,但我必須事先強調,每個學科領域或是每個人所適用的文獻閱讀方法絕對不同,如同做研究一樣,可以將別人的方法當作參考,但絕對不可盲目當作聖經完全依循,閱讀的同時也可以思考看看那些部份可以改進得更好,那些地方其實是做虛工浪費時間。畢竟,身為研究生,不論是哪種題材,閱讀時還是得保有一些職業敏銳度的XD。


I.  怎麼找?搜索文獻

還記得我當初剛進到碩士班,接觸到的第一個題目是我以前在大學時期完全陌生的領域(廢話一句XD),雖然指導教授並沒有給予任何壓力,但自己還是希望能快點從一張白紙到能夠聽懂group meeting時其他學長姊到底在說些甚麼。因此那段時期就一直逼自己大量閱讀期刊,希望能夠早點跟上其他人的腳步。


但是,要看那些期刊呢? 對於這種新手村LV0的狀況,最快上手的方式就是直接找看看有沒有這個領域的回顧文章,如果有的話,恭喜你! 你已經找到進入這個領域的guide paper了。若不幸地,你所研究的領域太新,或是太冷門,沒有這種Review Paper的話該怎麼辦呢?我個人的方法是隨機的挑選領域內十年內的文章大約十幾篇,只看Reference的部分,挑出這些文章都有引用的論文,基本上這些文章應該就算是這個領域比較經典或是重要的文章,可以把這幾篇文章當作是guide paper。從這幾篇經典文章開始出發,透過非常好用的google scholar,就可以找出還有那些文章是引用這幾篇經典文章的, 這樣就可以快速地建立出關於這個議題或是領域的文獻樹,快速瀏覽這些文章的標題,可以概括的對於這個領域在不同期間所關注的議題有一些基本認識,再從其中挑選有興趣的文章來看,能夠比較快速且有效率的幫助你在完全陌生的領域中梳理出方向。

這個階段的文獻瀏覽我只關注題目內容與作者是誰,頂多看到有興趣或是直覺認為重要的文章會多看一下摘要。因為在這個階段我只有三個目的:

  1. 試圖了解這個領域整體的發展方向與各個時期的主要研究重心在哪方面。
  2. 在這個領域內有那些人或是那些團隊是比較活躍的。如果有不同的說法或派別,那哪種說法或派別到現在還是比較流行的。
  3. 透過這種粗略的瀏覽先對這個領域建構出一個簡單的架構,再從中思考其中那些部分是自己有興趣的,哪些部分是重要的,進而再決定自己閱讀的重心該放在哪邊。
我個人是覺得找出自己有興趣的方向和了解哪些方向是這個領域重要的題目都一樣重要。前者能夠激發你的熱情,即使這個方向和你(或指導教授希望)未來的研究不同,都還是要趁業餘的時間好好研究一下,只要有熱情,學習起來的效率是連自己都會怕的;後者則是要有一些基礎的認識,了解其重要性為何,甚至要認真地翻過一兩篇經典的文章。


II. 怎麼看? 閱讀文獻

在回答怎麼看文章之前,我會希望先回答為什麼要看文章?沒錯,動機很重要。

看文章的目的不外乎兩種:1. 心中有疑惑,想要透過文章來獲得解答。2. 增加自己的知識庫。當然還有那些課堂作業或是老師要求的那種就先不討論了。

對我來說,我會認為閱讀文章有兩種方法,一種是橫向的閱讀,一種是縱向的閱讀。

橫向的閱讀是指同時將同一個方向的文章抓出來,在開始每看一篇文章前,自己心中先就這一批文章的題目做一些想法上的猜測,例如這些文章都是試圖解決哪個問題,如果是自己,會有那些解決的方法,或是看完這些題目後自己產生了那些疑問。將這些猜測和疑問記錄下來,再開始進行一輪快速的閱讀,這輪目的只是要回答自己的猜測是否正確,疑問是否能在這些文章中獲得解答。而在閱讀的同時,一定也會產生一些新的想法和猜想,這些想法或許可以修正自己最初的假設,也可能帶來更多的疑問,都無妨。經過一輪快速閱讀後,一定可以發現有幾篇文章對於你當前的問題幫助其實不大,可以不必再看,也一定有一些新的文章是你認為有幫助的,那就抓進來一起閱讀。接著再帶著這些問題再重複看第二輪。第二輪的閱讀同樣不求精,但可以比第一輪更有深度,同時也可以仔細的安排閱讀順序。例如通過第一輪的閱讀,大概可以知道那些文章都提到了類似的概念,有時候在A文章中對於這個概念有比較詳盡的解釋,B文章則是舉了一個不錯的例子或有仔細地推導,因此可以先讀A關於這個概念的部分,再看B文章的推導會比較有概念,或者是AB兩個交錯閱讀也是很好的方式。

因此,透過橫向閱讀的策略除了能夠透過不同文章的解釋讓讀者能挑選最適合自己的方式去理解外,也能夠逐層篩選出那些部分是自己最需要,非懂不可的部分,那些部分則只需要有個大略的概念即可,哪些部分則是可以完全忽略不理。也就是說,雖然乍看之下橫向閱讀需要耗費更多的時間進行閱讀,但這些閱讀都是更有效率的,因此能提供最多、最有用的資訊量。而且橫向閱讀的優勢在於每經過一輪閱讀,你對於這個領域的知識又增進了一層,自然能夠發現一些更為精緻的問題,再針對這些問題重複閱讀,必能對文章有更深一層的體悟。

縱向的閱讀則比較傳統,也就是一口氣認真仔細地讀完一篇文章。讀完後再針對文章內有興趣的部分或是有疑惑的部分去找尋第二篇文章來繼續閱讀。這樣的閱讀方式我個人是覺得比較偏向任務型取向的閱讀,例如為了應付某門課的報告,或是針對一個很明確的問題,需要在極短時間內獲得解答。

也有些人在閱讀文章時分為精讀和略讀。對我來說,除非這篇文章我很肯定與我目前工作的主軸有密切相關,或是這篇文章我有相當濃厚的興趣,抑或課堂需要進行報告,否則我不會精讀這篇文章。這並不是說精讀不如略讀,而是因為每個人時間有限,不可能每篇文章都能鉅細靡遺地從頭讀到尾,所以如何取捨閱讀材料也是研究生涯中需要練習的重點。

另外,我覺得不論是橫向或縱向的閱讀策略、精讀或細讀的閱讀方式,都有一個核心是必須抓住的,就是想像。很慶幸自己在閱讀第一篇文章的時候,就在無意間發現這個方法。當時為了某堂課的報告,花了一個星期一字一句地從頭到尾將文章仔細看過後還是不知所云,隨著報告時間一步步逼近,我就憑藉著我第一次閱讀後的印象,大膽地自己編了一個故事,想像這篇文章的重點是怎樣怎樣,然後用了哪些方法想要證明那些東西,因此得到那些結論。將我想像的大綱列出來後,在重新回頭去從文章找訊息,看看有那些敘述或是圖表是支持我所想像出來的故事,或是有沒有哪些敘述或是圖表和我的假設相反,那我該怎麼修正我的故事。因為已經閱讀過一遍了,對於文章的內容已有一些初步認識,所以很幸運地透過這個方式幫我理解了原本讀完第一次還是對其內容不知所云的文章。
也因此我會覺得在閱讀文章前,可以先閱讀完題目、摘要、各段落的題目後,再粗略地掃描一下文章內有的圖與結論,就可以開始想像、猜測這篇文章經營的步驟、方法以及結論,並試著自己架構出這篇文章的中心邏輯。然後根據自己的猜測從文章中找尋支持你的證據,如果發現與想像符合的話,那麼閱讀的速度就會飛快,因為你等於已經抓住作者思考的脈絡了;如果你發現越讀越奇怪,那可能是你的猜想錯誤了,這時候最好先暫停閱讀,重新看一遍摘要或你覺得重要的部分,再修正自己的猜想。透過想像建構自己的閱讀邏輯會比從無到有被迫去跟隨作者的思考邏輯還要輕鬆容易,只要持續發現自己的想法和文章內容相符,閱讀起來就不會有障礙,伴隨而來的成就感也能加大閱讀的樂趣;萬一自己的想法和文章內容相牴觸,因為需要重新思考與建構新的邏輯,也能加深自己對於這部分的印象。

怎樣才算是讀完一篇文章呢?根據你的目的不同會有不同的答案。不過有幾個基本的問題在你讀完這篇文章後一定要能夠回答出來:

  1. 這篇文章的重點是甚麼?它的價值在哪裡?它解決了那些科學問題?如果這個科學問題也有其他人進行類似的研究,那他們之間的差異在哪邊?
  2. 作者在解決這個問題的時候,有沒有進行一些假設?如果有,你覺得這個假設是否合理,或是這個假設有那些使用上的限制。如果作者沒有特別提到,那就針對這個問題自己假設一個非常極端的例子,看看文章的結論在這個極端例子中是否仍然成立?
  3. 作者使用了那些證據或是方法來佐證他的理論,是否合理?

最後,我的建議是最好能夠一次騰出數個小時,一氣呵成的閱讀。因為閱讀的時間越分散,思考就越不連貫,連帶降低閱讀的效率和印象。所以如果非必要,最好別利用零碎時間來讀文獻。


III. 怎麼記? 快速搜尋


我想大部分人在讀文章的時候,都會忍不住手癢畫畫重點寫寫心得。這當然是好事,在閱讀當下利用自己的思緒針對每一個小段落用自己的文字進行總結,或是隨手記錄下突如其來的想法與疑問,這些都是閱讀過程中最為寶貴的部分。
不過隨著科技的進步,每個人閱讀文章的管道越來越多元,有些比較環保的人會選擇在利用電腦或是平板閱讀文章;有些比較傳統或是害怕傷眼睛的人,就還是選擇使用紙本閱讀;甚至有人兩種方式都使用,雙管齊下的閱讀。這些方法都好。但是在這麼多元的媒介中,你會怎麼去記錄你的筆記呢?
或許有人會覺得是廢話,用電腦閱讀當然就直接在PDF檔案中進行編輯阿;用紙本當然就拿支筆直接在紙上塗塗畫畫阿。老實說,關於這點,我其實沒有太多的建議,怎樣紀錄當然依照個人習慣不同而有不同的方式。不過我只想點出一個很重要的概念:資訊集中,利於檢索。

嗯,沒錯,是八字箴言,絕對不是抄馬先生的。

就我個人的經驗,不論是紙本或是電腦我都試過。可能是我對於尖端科技的掌握度還不太熟悉,總會覺得在電腦上想要做些筆記或是畫線,還需要移動滑鼠選取功能鍵,然後才能畫線或是插入註釋,實在很不直觀,而且常常經過這些操作後,反而讓我分心,輕則忘記我本來靈光一現的想法,重則讓我自我放棄關掉閱讀文章的視窗。除此之外,因為領域的關係,做研究需要大量的coding,因此已經需要長時間面對螢幕了。所以最後我還是選擇使用紙本閱讀。而紙本閱讀時,如果有想法就可以直接寫在空白處,或是特別準備一本筆記本,把自己認為重要的訊息寫進去。

等等,這和那八字箴言有甚麼關係?

其實這主要是想要說明資訊集中的重要。如果你習慣使用電腦,那就盡情使用電腦記錄下你的筆記,如果你習慣紙本,那就好好找個筆記本,或是把你讀過的紙本都好好蒐藏。千萬不要一下把重點記在電腦中,一下又把心得寫在筆記本上,然後畫線畫在紙本中。這樣哪天你需要複習的時候,就會發現怎麼甚麼資訊都找不到,最後因為貪一時之快,反而得將文章重讀一次。紀錄的方法各有優劣,只要能確保資訊不會散落在各處就好。

此外,對大部分的研究生而言,所有的研究最終都得要寫成論文或是投稿期刊,在撰寫自己的文章時免不了需要進行文獻回顧。此時,如果能夠快速地找出過去自己所讀過的文章重點,將可以省下許多不必要的搜尋時間。同樣地,每個人紀錄的習慣不同,因此在找回過去閱讀文章的重點時所適用的方法也不盡相同。對我而言,我個人的習慣是在閱讀文章時是透過紙本閱讀,重點與心得也是直接寫在紙本上,但當同一篇文章讀過了一兩輪後,有些讀第一遍時的疑問可能已經獲得解答,或是經過更仔細閱讀後,發現有些第一遍略讀時的心得不全然正確,因此又直接在原本的筆記旁直接修正,如此一來,整個紙本就會變得相當混亂,不利於日後回顧。所以我會在一篇文章閱讀完畢後,如果有時間就趁記憶正清晰的時候將這些筆記或重點直接整理到電腦的PDF檔中,然後再放上雲端;若是沒有時間,就趁一些零碎的時間把這些筆記歸檔,在歸檔的同時也等於再度把這篇文章的重點複習一次。歸檔到電腦中的好處在於日後當我急需某篇文獻中的某一些句子或是重點時,可以快速藉由一些管理軟體,如Mendeley,幫助我很快的找到我需要的資訊。對我來說,不論讀的當下多麼認真,經過時間一定會忘掉許多細節資訊,因此把所有資訊透過電腦軟體集中儲存,日後需要的時候能夠快速找到,對我來說是比較有效率的紀錄方式。



2016年3月14日 星期一

濾波器發散 (Filter Divergence)


    系集卡爾曼濾波器(Ensemble-based Kalman Filter, EnKF)是利用系集預報後,各系集成員間的差異來估計背景誤差結構,而EnKF的表現取決於系集能否完整的表達真實大氣的特性,這牽扯到模式的動力是否完整正確、系集的樣本數是否足夠等因素。實作上因受限於計算資源,系集樣本數不可能太大,故EnKF經常面臨樣本數不足的問題。當系集樣本數不足(註一)時,除會引入取樣誤差外,更可能造成濾波器失敗(Filter Failure)。
  1. 較傳統的定義是指系集在動力模式中發展時,因樣本數不足,使得系集的差異性(離散度)不足,低估真實大氣的不確定性。這會讓EnKF認為此時的預報不確定性很低,模式已可掌握真實大氣的發展,也就不需過多的觀測資訊修正背景場。少了觀測資訊的修正,系集的離散度會隨著時間繼續下降,使得觀測資訊更難藉由同化過程修正背景場,最終致使系集收斂至模式動力特徵,完全背離真實大氣的狀態,導致濾波器發散(Filter Divergence)。 
  1. 另一種濾波器失敗稱作Catastrophic Filter Divergence(CFD),同樣是因系集樣本數不足造成。這種形態的濾波器失敗是模式在積分過程中,會於短時間內出現無限大的值(machine infinite),讓積分結果爆掉(blow-up),屬於數值不穩定造成的濾波器失敗。 
    Catastrophic Filter Divergence的觸發條件是:
  1. 觀測誤差很小,確保觀測資料一定會被使用。傳統的濾波器發散是因為觀測誤差相對背景誤差過大,使得觀測資訊無法被使用。而對CFD來說,反倒是需要很小的觀測誤差,除了確保觀測資訊一定會被使用外,因為觀測誤差很小,使得背景場會大幅度地朝觀測值靠攏。
  1. 需在特定的同化間隔(註二)才會觸發,當同化間隔過高或過低都不會觸發。若同化間隔過高,則分析場會非常接近觀測場;若同化間隔過高,則分析場會隨著積分過程而具有模式動力的特徵。但若是同化間隔不高也不低時,當獲得一個接近觀測值的分析場後,隨模式積分一小段時間,還沒獲得模式動力平衡時,又馬上被拉回觀測值。如此周而復始的在觀測值與模式動力平衡間來回調整將會誘發數值不穩定,進而讓結果在積分過程中爆掉(blow-up)。


註一:怎樣的系集樣本數才足夠?理論上,一個系統自身的不確定性可以用Lyaponov exponent來表示,假設某系統的Lyaponov vector是50,表示只要有50組獨立且有效的系集成員就可以表達此系統的不確定性。但實作上Lyaponov exponent 並不容易獲得,也沒有意義(因為NWP模式的複雜度太高,即便知道Lyaponov exponent 的大小,也沒有能力負擔相對應的系集數)。


註二:同化間隔是否適當取決於模式解析度與天氣系統的尺度。如氣候模式與雲解析度模式適用的同化間隔就不一樣。

<參考資料>
Harlim, J., and A. J. Majda, 2010: Catastrophic filter divergence in filtering nonlinear dissipative systems. Communications in Mathematical Sciences, 8, 27–43, doi:10.4310/cms.2010.v8.n1.a3.
Gottwald, G. A., and A. J. Majda, 2013: A mechanism for catastrophic filter divergence in data assimilation for sparse observation networks. Nonlinear Processes in Geophysics, 20, 705–712, doi:10.5194/npg-20-705-2013.


    濾波器失敗可以分為兩種不同型態: 
    這兩種濾波器失敗的型態雖都稱作濾波器發散(filter divergence),但對NWP而言,意義卻不太相同。第一種(傳統)濾波器發散發生時,模式仍能提供預報結果,只是預報結果完全背離真實大氣狀態,不具預報能力。若沒有仔細評估預報結果,可能會錯信預報結果。第二種濾波器發散是因為積分過程中引發的數值不穩定而造成。由於傳統的濾波器發散的發生有跡可循,可透過各種統計方法估計離散度或是透過背景誤差矩陣的擴張(Inflation)來避免。相對而言,第二種型態的濾波器發散就較難掌握,因此將就其觸發條件與發生原因進行討論。

    可以想像模式在積分時,是沿著模式動力的吸引子移動(達到模式動力的平衡),但因模式具有模式誤差,使得模式動力的吸引子並不相等於真實大氣狀態的吸引子,所以需要藉由資料同化將將預報從模式動力的吸引子拉向觀測值,以逼近真實大氣狀態的吸引子。(關於吸引子的概念,可以從Lorenz的蝴蝶圖想像,該圖具有兩個明顯的吸引子,可以類比左側的中心是模式動力吸引子,右側是真實大氣狀態的吸引子) 。因此若觀測資料無法被同化,將造成預報最終會收斂至模式動力的吸引子,完全遠離真實大氣狀態的吸引子,也就是第一種濾波器發散。若是觀測誤差很小,同化時,原本受模式動力影響的背景場會馬上移動到觀測附近,開始積分後又會從觀測附近朝模式動力的吸引子移動。如果同化間隔很小,代表預報結果剛從觀測值往模式動力吸引子移動沒多久,又馬上被拉回觀測值,因此能確保預報結果很貼近觀測值;若同化間隔很長,那就有足夠的積分時間將預報結果從觀測值拉回到模式吸引子。但當同化間隔介於兩者之間時,會造成預報已經在前往模式吸引子的路上,又突然被拉回觀測,然後再積分一小段後,又再被拉回觀測,使得預報不斷在模式吸引子與觀測間快速來回調整。這種快速且劇烈的調整使得積分過程變得相當不穩定(數值上),可能在某個同化完成後的積分過程就爆掉了。而且CFD並不是因為不穩定度累積超過某個門檻後觸發的產物,其發生機率是屬於Poisson Distribution,也就是隨機過程,並不能夠掌握何時會出現,因此在執行EnKF同化時,需特別注意同化間隔對於數值穩定度的影響。

2016年1月25日 星期一

如何提升程式可讀性

程式碼應易於理解。


因此撰寫程式時,應將讀者理解所需的時間降到最低,意即程式碼需具有最佳可讀性。此處所謂的完全理解是指讀者有能力修改,debug,並知道程式與其他部分互動的方式。程式的可讀性與效能最佳化兩者間未必衝突,甚至具有良好可讀性的程式因易於被理解、測試,將更有利於最佳化。然而可讀性的困難之處在於必須時時考量某位虛擬讀者能否理解,故初期(未養成習慣之前)會耗費更多時間,一旦建立風格後,將可大幅縮短額外耗費的時間。


可讀性的改善可由下列兩種概念著手:1. 表層改善。使程式碼在視覺上具有良好的可讀性,使讀者不會抗拒閱讀程式碼。2. 簡化迴圈與邏輯。使程式碼在心理上具有良好的可讀性,過於複雜的迴圈與邏輯、大量的變數都會提高讀者的心理負擔,需要更縝密思考和記憶更多變數才能理解程式碼,不利閱讀。


<表層改善>
表層改善的方法主要有三種:1.挑選合適的程式(變數)名稱,可使讀者迅速了解程式的目的。2.良好的註解,可適時的幫助讀者了解程式的運作。3. 具備良好程式美學,簡潔整齊具美感的程式碼能提升讀者的耐心。


  1. 合適的程式(變數)名稱
一個好的命名除了不會被誤解外,還要能提供足夠資訊使讀者瞭解,因此命名時需讓程式(變數)名稱具有意義,使名稱可成為註解的一部分。在完整表達含意之餘,如能兼具高資訊空間比將會是個成功的命名。

選擇詞彙時,須選擇意義明確並避免使用空洞的詞彙。因此善用英文的同義字,找出更合適、詞意更鮮明的字彙是命名時需費心的地方。同時,避免使用過於通用的名稱(如tmp),這些變數不僅有可能在程式碼中重複出現造成讀者混淆外,也不能提供額外的訊息,因此並不是理想的命名。然而,若此變數的生命期僅有數行,且此變數不具其他責任,不會被傳遞至其他函數,亦不會被重設或重複使用,在不影響讀者理解的前提下,為了撰寫者的便利還是允許使用這類變數。

最佳的命名應能說明變數的目的以及(或是)包含實體數值的訊息。如迴圈循環子(interator)的命名一般都是使用i,j,k,故在程式碼中若出現這些變數,讀者會直覺聯想到是interator,此種命名便清楚說明變數目的(但若是將i,j,k用在其他用途,就可能是個失敗的命名方式)。有時不得不在迴圈系統中使用許多巢狀結構時,適當命名interator也可協助讀者了解目前正處於哪段迴圈中。例如有三層迴圈,分別是觀測資料的迴圈、模式水平格點與垂直格點的迴圈,若將interator命名成i_obs、j_x、k_z,將有助讀者理解。

有時變數所存取的資料會有不同單位(或是不同屬性),如在命名時就提供這些資訊將可減輕讀者負擔,亦能幫助撰寫者除錯。例如時間相關變數,若單純命名為time,則可能在A段程式碼中讀取的時間單位是秒(sec),但B段中讀入的時間單位是毫秒(ms),如此不僅會混淆讀者,甚至是個潛在的bug。如將單位加入變數名稱,如time_s、time_ms,便能方便辨識。

因此,良好的命名不僅可增加可讀性,亦可避免bug的發生。命名時需跳脫撰寫者的身分,發揮自身想像力思考名稱是否具足夠資訊協助讀者了解此變數/程式碼的目的,並審慎思考該名稱是否過於空泛、模糊、間接,使讀者不僅無法一眼看出目的外,甚至造成讀者誤解。
為使命名能提供足夠訊息,在合適範圍內增加名稱的字元數不是壞事。一般命名原則是較小的範圍內可考慮使用較短的變數,若變數跨越的範圍較長,則可透過較長的名稱賦予較多訊息以利讀者理解。因為若變數生命期只有數行,名稱卻有20個字元,其所提供的資訊空間比就較低,不是理想的命名方式。較長的名稱對撰寫者而言無疑是個負擔,但大多數的編輯器都內建「識別字補齊」的功能,如在vi中,便可使用ctrl-p 幫忙補齊字元,在註解中也同樣適用。但要注意,並不是長的變數名稱就一定好,有時消除變數中的某些詞彙若不影響名稱所含的資訊量,就建議移除(如fromNumberToString,若拿掉from並不影響意思,故寫成number2string或num2str即可)。在不同型態的變數使用不同的命名規則也有助於讀者理解,如在類別名稱使用駝峰式大小寫,變數名稱使用底線分隔也是種方法,總之只要確立命名原則並妥善遵守(或是提供註解說明)都能增加可讀性。

有些撰寫者在命名時會使用縮寫,如rmse,若這種縮寫對同領域/專案的使用者而言相當常見(如在資料同化中,使用B代表背景誤差協方差矩陣),那就可被接受(然而,在程式碼中若直接將背景誤差協方差矩陣命名為B也不洽當),否則,還是盡量避免。

為避免造成使用者誤解,有些基本的命名原可作為參考,若數值包含邊界,可加上max_或min_,便可清楚知道數值的特性;變數名稱最好避免使用否定敘述,以免增加閱讀負擔;如能在命名時便告知使用者大概的計算量,那也是相當有用的資訊,例如在進行龐大計算時,可使用compute而非get(computeMean)。


  1. 良好的註解
註解的目的是協助讀者能快速地了解撰寫者的想法,同時提供撰寫過程中的經驗。一般而言,程式碼的名稱應能使讀者對此程式具有基本概念,然後程式碼的開端可提供詳細、更進一步的文字說明(全局註解)。每個大段落間也可透過註解告知讀者此段程式碼的目的,避免讀者深陷於細節之中。在撰寫註解時,並不一定得留下正式完整的文句,只要能提供重要的訊息,簡單的幾句話也可以。

程式碼中的註解除幫助讀者瞭解各區塊的目的外,也提供平台供撰寫者與讀者對話,如果撰寫方式因某些原因而和一般習慣用法不同,即可透過註解說明;如果撰寫者針對某部分的效能已有一些測試結果,同樣可以註解方式說明,避免維護者或後續使用者重複無謂的測試;定義常數時,對各常數的使用環境以及採用特定數值的原因均可透過註解使讀者瞭解;撰寫過程中,有時為了時效而忽略排版或效能,這些不足之處可通過註解說明改善方向以供後續使用者(可能是數週後的自己)跟進。一個良好有用的程式碼應會不斷被使用,也會持續地被改善,故於撰寫過程中忠實記錄發現的缺憾或改善的構想能讓讀者瞭解程式的狀態及品質。許多程式設計師會使用下列標記紀錄不足的部分:
TODO:紀錄作者尚未處理的部分 (若是大寫表示須優先處理,小寫則表示問題較小)。
FIXME:已知的問題。
HACK:承認解決方式不夠優雅。
XXX:危險! 相當重要的問題,需要立即解決。

然而,並非每一步驟都需要註解。技術上來說,若註解沒有提供新資訊,或註解的部分只需簡單觀察程式碼即可輕易知道工作內容,那這個註解就是無用的註解,並不需要為了註解而註解。如何決定一個註解是有意義的?除了上述的情況外,程式設計師會試圖從第三者的角度來重讀程式碼,預期讀者可能會出現的問題。若在重新閱讀時,發現某些程式碼的內容需要額外仔細思考,或對某些變數的存在感到疑惑時,就非得要註解不可(如果是對變數的命名感到疑惑,此時需要的不是註解,而是重新命名)。

簡單的說,適時寫下心中有意義的想法,就算只有幾個字也好,都可有效幫助讀者理解程式碼。但一個好的註解應具有「高資訊空間比」。與命名的原則相同,能妥善使用包含大量資訊的詞彙將可使註解更為簡潔;撰寫註解時需極力避免模稜兩可的的代名詞(如修好它,讀者並不一定知道"它"表示哪一部分);使用非內建或陌生函數時,必須精確描述函數的行為,最好提供輸入值與輸出值的範例(以及變數型態)以增進讀者瞭解,同時亦可幫助除錯。


  1. 程式美學
良好的程式碼應能使讀者賞心悅目,因此程式碼編排方式對可讀性相當重要,基本上可藉由一致的排版、將相似(關)的程式碼調整成有相似外觀以及將程式碼分段使程式碼具有基本美學架構。

為了排版的一致,在整個程式或專案中,最好維持相同的排版原則,如巢狀結構中空白字元的數量、段落間空白幾行等。為使整體版面能維持整潔,多幾行程式碼無傷大雅。

將相似(關)的程式碼調整成有相似外觀,如此除能使讀者一眼便可了解此區塊的程式碼具相同結構外,由於對應的位置相似,若出現打字錯誤的bug,也能夠快速找出。然而,若要將大量的程式碼調整成「列對齊」以提供較佳的視覺邊界,對撰寫者而言也是不小的負擔。當維護時,原本只是修改一行程式碼,卻為了使整體版面具有相似外觀反而需改動更多行程式碼。雖然麻煩,但這樣做的好處應遠大於所花費的時間。萬一程式碼真的過於龐大不易調整,到時再放棄就好。

適當的分段能讓讀者清楚知道目前所在位置,同時也易於閱讀,若能於每段都放入合適的註解將可大幅縮短讀者的理解時間。在某些情況下,程式碼的順序並不影響正確性,然若能賦予有意義的順序對程式維護與內容理解將有正面助益,透過重要性遞減排序,或是單純依照字母排序都是不錯的方式。

程式美學的標準見仁見智,但在專案中,保持一致的風格會比「正確」的風格更為重要。



<簡化迴圈與邏輯>
如果程式碼中沒有任何的迴圈或邏輯判斷,讀起來會非常容易,過於複雜的迴圈與邏輯判斷將會增加讀者的負擔,因此如何簡化程式碼中的迴圈與邏輯敘述對可讀性的提升相當重要。設計迴圈與邏輯敘述時,應使敘述盡量自然,讓讀者在閱讀過程中不需停下來重新思考。例如一般的程式設計者在設計迴圈與邏輯的條件敘述時,習慣將會改變的變數(也就是欲比較的對象)放在左側,而固定的常數(也就是比較的基準)放在右側,如果違背這個原則,讀者在閱讀時將會因程式碼的「不自然」敘述而停下腳步,雖不至於無法理解,但會降低閱讀速度。在撰寫if/else敘述時,通常能自由交換各區塊的順序,但若能先處裡肯定條件而非否定條件、先處裡簡單的情況、先處裡較有趣或明顯的情況,將可避免條件敘述是以較難理解的順序呈現,也是增進可讀性的好技巧。


有些程式設計師喜歡將複雜的概念用一行程式碼或是獨特的程式碼來表示,前者對讀者而言需要花費許多心力才可能領悟撰寫者的巧思;至於後者,雖然寫法獨特的程式碼看起來很新穎,也許能縮減程式碼行數,但會對讀者以及後續使用者造成困擾,如果是個人練習的實作無可厚非,但若是需要分享的程式碼,還是以增進程式可讀性為優先,縮短他人理解所需的時間比減少程式碼還重要。


多層巢狀結構對讀者相當不友善,每層巢狀結構都會造成讀者額外的心理負擔,因為當讀者看到某層巢狀結構結尾時,很難回憶起這部分屬於哪層巢狀結構以及其所表示的意義。所以除非必較,撰寫時盡量避免使用過於複雜的巢狀結構。巢狀結構的增長往往出現在修改的過程,重複的增修經常會導致最後出現複雜的巢狀結構。因此修改程式碼時,需以全新的角度重新審視,以整體的角度考慮程式碼的修改方式。一般來說,非得使用巢狀結構時,最好能及早跳脫並清除巢狀結構,使程式碼更為簡潔,因為每多一層巢狀結構,讀者就必須記憶更多的環境資訊。盡量使用線性的程式流程,避免巢狀結構。


和100行程式碼相比,一次閱讀3000行程式碼對讀者造成的心理負擔更重,研究顯示人類一次只能思考三到四種東西,因此程式碼越長就越難理解。在撰寫程式碼時,最好將巨大的內容分解成更容易消化的大小。這個概念相當重要,可應用於任何層級的程式碼中。最低階的層級如某行需要計算的程式碼,假設該行程式碼的工作是先讀取某個變數,再進行條件邏輯判斷,若只用一行表示會增加讀者閱讀負擔,故可使用「解釋性變數」先行讀取變數,並以變數名稱告知讀者正在進行的事,再使用第二行程式碼進行邏輯判斷,雖然這會多出一個變數及一行程式碼,但對整體程式碼的負擔並不顯著,且能大幅提升可讀性;相同的概念,假設某個變數陣列中有五個變數,但計算過程中只需使用其中某變數,此時便可利用「摘要變數」來增加可讀性。摘要變數目的是用較短、較易了解的名稱,於小範圍程式碼中取代較長或複雜的變數名稱。利用這兩種技巧,可避免在程式碼中不斷重複相同的步驟(DRY, Do not Repeat Yourself),不僅能避免重複打字造成的錯誤外,也可大幅減少程式碼的負擔,維護時若欲修改名稱也只需更改一次。


更高的層級是思考該如何重新組織整個程式碼,找出程式碼中會重複執行的區塊,並試圖用副程式來處理。這樣的好處不只能縮短程式碼,將程式內容以副程式獨立出來也易於維護與測試,甚至副程式還能被其他程式呼叫,成為通用的副程式碼。在重新組織程式碼時,有兩個很重要的概念:1.積極尋找並抽離不相關的子問題2.一次處理一個問題


在程式撰寫的過程中,有個相當重要的基本原則就是將大問題分解成小問題,再將小問題的解答組合成原本大問題的解答。因此如何找尋可分離且值得分離的部分就是個值得程式設計師認真思索的問題。初學者的話,可先閱讀特定的函數或程式區塊,並自問這段程式的主要目的為何?接著再細看每行程式碼,自問每行程式的工作內容與這段程式碼的工作目的是否直接相關?還是這行程式是為了處理與目的無關的子問題?若用以解決子問題所花費的行數過多,就值得抽離並寫成獨立的函數或副程式。例如在計算兩點間距時,需處理球面的影響,座標轉換的過程相當複雜且一再重複,所花費的程式行數也相當龐大,故若將此部分以副程式表示,即可簡潔程式碼,並使撰寫者能專注於高階目的,不需一直耗費心思處理幾何公式,且這個副程式也可被重複使用,是個相當值得的投資。同時,將子問題抽離後,也較容易加入新功能、改善可靠度或進行效能測試。因此一個理想的專案,應會將許多常見的基本子問題抽離,並放置於特定資料夾供所有撰寫者使用。


但須注意,萬物皆過猶不及。若將程式碼分解過頭反而會造成讀者的負擔,讀者必須記憶各副程式輸入與輸出的訊息,也可能需要在執行的路徑上來回跳躍。因此撰寫者必須清楚了解加入新的副程式必須付出微小(但明確且無可避免)的可讀性成本,若付出的成本並無法獲得相對應的收穫,就不需要立刻將此部分寫成副程式,可等到其他部分也面臨同樣需求時再來考慮。


如前所述,人的大腦一次能思考的東西並不多,若在程式碼中同時進行許多工作,將對讀者造成嚴重負擔,對撰寫者而言也可能是個潛在bug。故應將程式碼重新組織為一次只做一件事(包含副程式),將一大段的程式碼依邏輯獨立性分拆成不同段落。實作前可先列出需進行工作內容(盡量詳細),再依工作間的相關性盡量將各工作獨立進行,使程式碼的結構在不顯著影響效能與保有正確性的前提下能更為單純簡潔。分離的形式並不重要,可以是抽離成新的副程式,也可以獨立成段,重點在於能確實分離。然而分離工作並非件容易的事,有時可能有五項工作,但囿於效能或其他原因使能將其分離出三項,但也是相當不錯的收穫了。


在希望增進可讀性之前,我們必須知道,可讀性最高的程式碼就是沒有任何程式碼。也就是說,一旦開始撰寫程式,就會立即面臨可讀性的問題。若想達到最佳可讀性,那程式碼自然是越自然、越短越好。要使程式碼更為自然,上述的抽離子問題與一次處裡一個問題的原則都可使讀者很自然地了解撰寫者的構思。但要使程式碼變短,並不能像存錢一樣,東省一兩行、西省一兩行,而是要更有組織,從結構上處理才是根本之道。因此必須掌握一個原則:不需要的功能不必開發。


經常在一個專案開始時,設計者都會很興奮地設想許多附加的功能,希冀這個完美的程式碼能夠具有無限擴充性,但往往反而會忽略專案真正的核心功能。最後這些附加的功能可能來不及完成,或根本沒人使用,反而增加程式的複雜度。因此想要縮短程式碼,必須從一開始設計時,就很清楚程式工作的核心目的,以及潛在的擴充方向,以完成核心部分為主要方向的前提下,再合理地維持擴充性。


開發過程中,隨著系統的成長,維持系統所需的複雜度會以等比級數增加。因此最好的方式是維持程式碼的小而美,也就是在開發過程中,盡量使用通用的副程式(適度的抽離子問題)以消除重複的程式碼,同時須果斷移除未使用的程式碼與功能,並隨時注意程式的份量。前者對大多數有觀念的撰寫者而言並非難事,但要求撰寫者自行刪除程式碼卻非易事。一旦程式碼被寫下之後,這些程式碼就代表工作的成果,刪除程式碼意味著之前所有努力全都白費,在心理上不易接受。當下定決心刪除程式碼後會發現,刪除獨立的函數相當容易,但有時無用的程式碼卻是散落在各個部分,不僅找尋不易,有時甚至連撰寫者都不知情。最常見的情況是程式設計之初,希望在核心功能外能搭配一些附帶功能,然而在撰寫過程中因專案目的改變或其他原因而放棄,但原先為此附帶功能而預留的空間或變數卻依然被保留,久而久之可能連設計者都忘記它們的存在,這些就是屬於無用的程式碼。


除了無用的程式碼需要刪除外,無用的變數也不需要保存。濫用變數或許能讓撰寫者快速地達到目的,但對讀者而言,變數越多越難同時記住所有變數;變數生存期越長,就必須記憶越久;變數若一直改變,就越難記得當前的數值。因此撰寫時必須刪除不必要的暫存變數,若該變數無法提供任何額外訊息,也無法簡化程式碼,就可考慮刪除此變數。在處理問題時,有時需要一些中介變數,使用這些中介變數時最好能盡量縮短它的生命週期,因此若能一次處理一件事情,並盡快完成所需的工作就能盡早刪除這些中介變數,避免這些中介變數成為潛在的bug。簡單的說,撰寫程式時,要盡量縮短變數的生命週期、減少變數出現的行數、也減少變數改變的次數,因為操作變數的地方越多就越難記得變數目前的數值。此外,也得避免使用過多的全域變數,以免全域變數汙染命名空間,使得修改區域變數的數值時,影響到全域變數的數值,產生一個潛在bug。


簡化的過程中除了依循上述三個概念外,有些不錯的小技巧也可幫助寫出簡潔的程式碼。例如在處理問題之前,先使用口語敘述遭遇的問題以及困難處,並說明可能的解決方法。如果發現無法用口語清楚描述,那表示可能遺漏某些東西,或仍有問題尚未被發掘。有時十分單純的問題,卻在實作過程中反而轉變為非常複雜的邏輯概念,通常遇到這種情況表示思考過程中遺漏一些重要因子,或其實有更簡單的作法只是還沒被想到。前者可透過口語敘述解決;後者則需具要些創造力找到更優雅的作法,一般可先嘗試能否從反向思考獲得解決方式。確定整體構想後,再依照構想寫下步驟並抽離子問題,養成這個習慣可幫助撰寫者在設計架構時能以更宏觀的角度思考並避免過度設計。

<參考資料>

易讀程式之美學-提升程式碼可讀性的簡單法則,2013,O'Reilly,ISBN:978-0-596-80229-5