Try   HackMD

A0002 見山是山,見山不是山,見山只是山

寫程式如禪學

「見山是山,見山不是山,見山只是山」[1],這是禪學領域知名的人生哲理,描述著人生會經歷的三個階段。其意思是,在人生年輕歷練尚淺時,看到山就是山,不會有太多的想法;當進入中年,有一定人生歷練的時候,看到山總是會有些不一樣的體悟出來,或許覺得壯闊、或許覺得莊嚴、或許覺得寧靜,端看山所帶給自己的感覺,而不是山本身;最後到了經歷更多的老年,在看透了許多事情後,看到山反而沒有之前的包袱,認為山就是山,沒有必要過度解讀。

這樣的人生三境界,在寫程式的領域也是如此。

境界一:見山是山

初次接觸寫程式的時候,通常都是從語法開始學起,所以看到語法就是語法,也就是見山是山。比方說,當學到了 if、for、class、function 等等的語法,心中所想的是,需要判斷式就加入 if、需要批次處理就加入 for、需要將資料包起來就加入 class、需要打包一段程式碼就加入 function。見山是山,語法就是語法,任何語法使用起來都是直觀的。

境界二:見山不是山

隨著寫程式的時間越來越長,不論是自己經驗的心得、還是前人提供的指導、或者書本撰寫的知識,對於語法的使用概念會越來越不單純。可能會有以下情境:

  • 得知「使用switch 比 if 有效率」,就開始覺得要把任何的 if 都替換成 switch。
  • 得知「用泛型做 for 會更有通用性」,就開始覺得 for 都要寫成泛型。
  • 得知「相關的觀念就一定要封裝」,就開始覺得不寫 class 罪大惡極。
  • 得知「不包 function 難以複用程式碼、可讀性低」,就開始覺得包 function 才是正確的寫法。

自此語法不再是語法,都無形地被延伸概念所取代,見山不是山。從上述情境可以發現,這境界的程式設計人員有很重的包袱,可能是擔心違背了哪個程式觀念、也可能是對自己沒信心,導致在看到不錯的程式寫法或觀念之後,就拼命套用、彷彿非要用上一堆方法才是寫程式的正道。當寫程式就是套方法、套公式的話,產出的程式碼通常不直觀。由於是套用公式,通常從某些來角度看,水準會還不錯,可惜寫起來稍嫌不夠精準[2];運氣不好的話,可能用得不恰當,反而把程式碼變得難以維護。

境界三:見山只是山

在經歷過許多的程式觀念、方法、公式的轟炸後,漸漸地回歸語法與觀念的本質,也就是定義。任何寫法都是在清楚定義、明白優缺點的前提下完成。心中所想的是,需要判斷式就加入 if、需要批量處理就加入 for、需要將資料包起來就加入 class、需要打包一段程式碼就加入 function。文字描述看起來與見山是山的境界沒有差別,但實際上要如何判斷「需要」卻是非常不同的。以 class 與 function 的例子來說,見山只是山的境界會很清楚要完成目標去包裝 function 或 class 的利與弊,判斷的準則都是從定義做起,然後必要就去做、非必要就不去做,而不是見山是山單純想到就去做

見山只是山的程式設計人員,對於語法與觀念的掌握度很高,所以處理問題的方法與工具會有很多,也因此總能產生很靈活的應對解決方案;各種精妙的程式碼,通常也是在這個境界才能寫出來的。這應該是最能享受寫程式的境界,不但思考的過程愉悅,完成程式後也頗具成就感。

Pointer 只是 Pointer

如果還是覺得這三個境界很抽象,沒關係,下面透過 C/C++ 的重要觀念, Pointer 來做說明。相信透過這個經典例子,可以更清楚寫程式的三個境界是什麼概念。

境界一:見 Pointer 是 Pointer

剛接觸 Pointer 的時候,於定義上會學到「Pointer 的用途是儲存位址」;因此當書本或課堂範例提到位址,就會與 Pointer 做連結。這樣的連結,通常像是在背課文一樣,看到 int* p = &a; 就翻譯成 pa 的位址。沒有什麼雜念,沒有什麼額外的想法,卻不知道應該在什麼地方使用。唯一可能使用的情境,就是當有些地方程式碼行為不如預期,像是想改變什麼值卻無效、或改變值卻造成 memory corruption,才會想到要調整 Pointer 的寫法試試。一個星號不對就用兩個,兩個星號還是不對,就再改寫法以避開 Pointer。由於很清楚 Pointer 的定義是儲存位址,所以見 Pointer 是 Pointer,只是可能不清楚怎麼使用,所以能免則免。

境界二:見 Pointer 不是 Pointer

可是在 C/C++ 的領域要完全避開 Pointer 是非常困難的,很多情境都會需要用到。在不知道什麼時候該用,什麼時候不該用的狀況下,最常見的使用方式就是套公式。就像學生寫數學題目,很多時候不求甚解,只要題型與對應公式背好,考試就套公式,反正能拿高分就可以了。

下面舉幾個常見的 Pointer 使用公式:Swap 交換兩個數值Array 傳遞額外的返回值。當然,會使用到 Pointer 的公式不止於此,這裡提出 Pointer 初學最常見的案例來做說明。

Swap 交換兩個數值

void swap(int* a, int* b) {
    const int tmp = *a;
    *a = *b;
    *b = tmp;
}

Array 傳遞

int sum(const int* p, unsigned int size) {
    int sum = 0;
    for(unsigned int i=0; i<size; ++i) {
        sum += p[i];
    }
    return sum;
}

額外的返回值

void complex_mul(int a_re, int a_im, 
                 int b_re, int b_im, 
                 int* c_re, int* c_im) {
    *c_re = a_re * b_re - a_im * b_im;
    *c_im = a_re * b_im + a_im * b_re;
}

如果有寫過與 Pointer 相關的程式碼,肯定對這些公式不陌生,或多或少會用過這些公式。在見 Pointer 不是 Pointer 的境界,其實對 Pointer 如何使用還是不清楚的,但在遇到這些公式的情境,就會很自然地將對應的程式碼寫出來讓程式行為符合預期。就因為該怎麼使用 Pointer 還不是很熟,所以在學習過的公式之外,依然是不碰 Pointer 的。

境界三:見 Pointer 只是 Pointer

進入到這個境界,使用 Pointer 並不拘泥於公式,僅針對定義「Pointer 的用途是儲存位址」就能發揮 Pointer 的各種效用了。或是用於通用介面、或是用於抽換 function、或是用於實現多型(Polymorphism)、或是用於隱藏實作等等。效用之多,不是隨意舉幾個例子就能概括的;各種靈活運用已經無法用套公式來辦到,而是在清楚定義的前提下,去建立自己想要的實作方案。一旦拋棄了公式的成見,就能見 Pointer 只是 Pointer,當用則用,不當用則不用。

終能見山只是山?

如前面所說,當到達見山只是山的境界,不但寫程式的思考過程愉悅,能寫出精妙的程式碼,而且完成後的成就感極高。那是不是未達此境界的人,只要程式繼續寫下去、繼續練習就能達成呢?答案是否定的。人生可惜的就是不一定能見山只是山,可能終其一生都還是見山不是山,見到山都還是有想法、有感慨、有包袱,看不見山的本質。寫程式也是如此,許多人寫程式陷在套公式的泥沼裡,實作方案永遠都在找相似題與公式,沒有脫離套公式的時候。

為什麼會停滯在見山不是山的境界呢?主要是套公式之餘,沒有去思考公式的組成原理、也沒有去理解各個原理的定義、更沒有在套公式之餘問自己是否有更好的做法。長久下來,再也不問程式觀念的定義是什麼,反正程式能動就好,自然無法見山只是山

其實停留在見山不是山的境界也不見得是糟糕的,畢竟每個人的理想與目標不同,非得在寫程式的旅程上追求最高境界是不合理的。倘若工作不需要、或者目的不需要,即使寫程式停留在最初的見山是山又如何?應該要看自己的需要,再來追求應該要達到什麼境界。

見山只是山

那要如何才能邁向見山只是山的境界呢?下面分享幾個具體訣竅:

  • 追本溯源:當接觸到新的程式觀念,不要只是將範例當公式使用,而是要釐清根本定義;縱使定義可能不容易理解,也可以先記起來。將來有機會再接觸到的時候,嘗試用定義思考、解讀,就會有豁然開朗的一天。
  • 他山之石:當看到別人優秀的程式碼,如果認識作者,可以請教作者這樣設計的理由;如果不認識,可以嘗試解讀,或者思考是否有更好的寫法。只有弄清楚為什麼要這樣寫、為什麼有(或沒有)更好的寫法,才能將好的方法內化成自己的技術。
  • 百尺竿頭:當自己完成一份程式碼,不要只滿足於完成的當下,永遠都要思考有沒有更好的實作方案。如果自己知道有,盡可能抽空練習將程式碼改進;如果自己不確定有沒有,也可以請親朋好友幫忙做程式碼審查(Code Review),不論審查結果如何,都能透過討論、接收更多不同的觀點,以豐富自己的知識庫。

條條大路通羅馬,其實要讓自己往下一個境界邁進的方法有很多,並不限於上述的訣竅,這裡僅就經驗提供作參考。只要有心要讓自己進步,不要一直覺得自己已經很厲害了、不要拒絕接收更多知識、不要拒絕檢討自己、不要成天只想著程式能動就好,總會有達到見山只是山的時候!


  1. 《指月錄》卷二十八。青原惟信禪師曾對門人說:『老僧三十年前未參禪時,見山是山,見水是水。及至後來,親見知識,有個入處,見山不是山,見水不是水。而今得個休歇處,依前見山只是山,見水只是水。』。 ↩︎

  2. 精準的程式碼,指的是易讀、易懂、易 trace,不殺雞用牛刀,不囉嗦地完成簡單程式碼。 ↩︎