函式的兩易一難

正文開始前,分享一段個人非常喜歡的演講:Philip Wadler的『Propositions as Types』:

其中有一段很讚的話:

Computer Science. There are only two things wrong with it: Computer and Science.

身為CS出身的碼農真的是被婊到心坎裡 …

這句話背後的意義是,我們真正在做的事情,其實是資訊的構築與轉換,計算機或程式語言不過剛好是我們用來處理這些問題的工具,也因此他認為較適合的名稱其實是Informatics之類的東東。什麼意思呢?例如Int版的toString如果用Haskell type annotation來寫,大概是

toString :: Int -> String

也就是從一個結構為整數的資訊到一個結構為字串的資訊的轉換。sort則是

sort :: Ord a => [a] -> [a]

從[a]到[a]雖然沒有發生資訊的類型轉換,但輸出的[a]會是排序過的。這件事通常不會表現在type annotation上,但硬要幹的話應該也不是不行;例如定義一個type叫OrderedArray,上式就變成

sort :: Ord a => [a] -> OrderedArray a

雖然骨子裡我們做的是如此充滿數學與工程的事情,也許正因為語言扮演要角吧?寫過幾年程式後,大家都會發展出自己的風格,為了避免一個code base被搞得像超現實主義的大雜燴,於是有了各式各樣的coding standard來做基本規範。一個好的coding standard,個人覺得會像是制定一個基本框架,讓大家能在維持基本結構統一的情況下發揮創意,就好像七言絕句或詞牌一樣,與各種programming paradigm交叉組合下,漸漸地寫程式這樣聽起來如此geek的事情,本質上好像與更為貼近了。

可能因為個人偏好functional programming吧?我寫程式喜歡以函式為單位開始,而不管用哪個語言,我寫一個函式的原則是兩易一難易讀、易測、難誤用

何謂易讀?

易讀與否其實是一個很主觀的問題。對我來說,所謂易讀大概就是兩點:

1. 我不須一直捲動程式碼即可掌握全貌
例如用Java或Swift寫一個class有兩種常見風格,一種是按照visibility來整理:所有public的東西放一區、protected的放一區、private的東西放一區;另一種則是按照關聯性來整理,例如一個public介面用了一個private helper function或一個protected constant,這些東西就會被放在一起。兩種整理方式都有其道理,但我個人偏好後者,因為我記憶力很差,實在受不了讀一個函式要一直來回捲動來找參照到的東西。

2. 不要用太聰明的語言技法來撰寫
呃…這也蠻主觀的。我的原則大概是盡量讓input與output的意義明顯,讓閱讀的人不需要花太多力氣就可以了解這個函式的意義是什麼。舉例來說,現在很常見的JS object spread operator就很容易寫出『太聰明的函式』,例如我們常常看到這種redux action creator:

const fooAction = ( args ) => ( {
    type: JUST_A_FOO_ACTION,
    ...args,
} );

這個action creator非常general,單看這裡我只能推論對應的reducer應該是舉凡JUST_A_FOO_ACTION來的東西一切通吃,偏偏事情通常是這樣:

const fooReducer = ( state, action ) => {
    if ( action.type === JUST_A_FOO_ACTION ) {
        return {
            foo: action.foo,
            bar: action.bar,
        };
    }
};

這還只是兩層,如果有好幾層的function call全部都spread來spread去,光是要知道一個key-value是怎麼來的就有得trace了。以上例來說,fooAction應該要這樣寫會更加清楚,也不會被誤用:

const fooAction = ( { foo, bar } ) => ( {
    foo,
    bar,
} );

我了解spread operator有它必須的時候,特別是在寫library code而非application code的時候,但我覺得必須了解它代表的語意是『將object A所有的properties都以相同的key賦至object B中』,而非因為很方便就濫用。

何謂易測?

這就比較客觀了:

  1. 容易寫unit tests測試
  2. 容易debug

前者通常衍生出來的性質是不仰賴特定全域變數、pure等等。例如WordPress裡面有很多函式是這樣:

function foo() {
    global $wpdb;
    $result = wpdb-> ...
}

這種code對我來說就很糟,想像看看這個foo要怎麼寫unit tests?光是要mock database和抽換$wpdb就是一大工程。

至於後者,以imperative programming language來說就是容易step by step trace,可以是debugger,也可以是塞log。因此,對我來說

const foo = ( value ) => {
    const stagedResult = someTransform( value );
    const yetAnotherOne = oneMoreTransform( value );
    const finalResult = combineTheTwo( stagedResult, yetAnotherOne );
    return finalResult;
};

會比這個來得好:

    const foo = ( value ) => ( combineTheTwo( someTransform( value ), oneMoreTransform( value ) );

如果哪天foo壞了,前者不管要塞log還是用debugger都非常輕鬆,但後者如果想要log常見的方法是先寫個這玩意

const wtf = ( arg ) => {
    console.log( 'wtf: ', arg );
    return arg;
};

用debugger也很容易操作錯誤,因為會需要不斷step in、step out、inspect return values,一不小心按錯就重來。

但經驗上,我寫前者一定被reviewer電到爆,所以我都從善如流寫後者 (淚)

何謂難誤用?

想像自己在經營一家餐廳,有一天其中一間廁所有了靈性,如果有超過10年經驗的碼農進去大X,馬桶就會悲憤爆炸,怎麼辦?把那間廁所上鎖不要讓人用就行了。

咦?可是又不是有那麼多超過10年經驗的碼農會去大X啊?問題是只要有一個進去就game over了啊,滿地金湯的餐廳能看嗎?函式也是一樣,如果是無法處理的input,最好的方法就是讓它不可能進去,不然就是要把它濾掉。

很遺憾的是,這件事很難在JS或PHP這類dynamic language做到盡善盡美。例如有時候我們會看到這種code:

function foo( $val ) {
    $val = is_int( $val ) ? $val : 0;
    ...
}

如果不是primitive type,而是composite type像array或object呢?檢查的cost太高,寫起來又麻煩,根本不會有人這麼做。常見的方法是用JSDoc之類的東西寫下一個function預期的input與output,但compiler並不因此就會保證這件事,所以使用者仍然可以『合法誤用』,因此稱不上完善。而且,如果把親愛的null和undefined考慮進來就更有趣了吧?我相信正在讀這篇文章的人,或多或少都被預期外的null炸過個幾次而怒吃雞排。Haskell或Elm選擇用Maybe這樣的product type來處理這個問題,C/C++的pointer type可以做到類似的事情,但效果並不好,因為

function foo( SomeClass* );

SomeClass*這樣的pointer type代表的語意其實是一個『dereference後會得到SomeClass的type』,但因為null pointer的存在,它多了一個『存在與否』的語意,寫過C/C++就會知道這樣看似小的尷尬語意能造成多少麻煩。

回到JS與PHP,個人覺得能做的大概就是primitive type有必要檢查就檢查,如果有值域的限制那就一定要濾掉値域外的數值,如果是object type那至少要意識到null,最後就是一定要寫unit tests來確保以上『語意保證』,畢竟dynamic language的compiler是不會幫你做這些事情的。

Code is Poetry

codeisshape_650_1024x1024

前面提到寫程式碼對我來說是件很有詩意的事情。當在一個一卡車人協作的code base工作時,這更像一首著意和諧的詩。如何找到個人風格與他人風格的平衡點?如何讓基本框架有效又不過於抑制個人發揮?又理性又感性,又獨特又和諧,種種激盪在開源的發展下愈發精彩。找到屬於自己,能帶到各種語言、各種專案應用的『武器』,或許正是這個時代當個碼農的醍醐味吧。

對「函式的兩易一難」的一則回應

  1. 那演講好硬呀~

    同為CS出身,我也覺得在學時有種怪怪的感覺,連問題都還沒好好先了解(敵人是誰、什麼情境下有仇),先教你用什麼工具可以解(不要管,拿著這把古靈精怪槍就對了)。
    難怪一堆應用數學系出身的一碰到程式語言,跟化學反應變身怪物一樣,衝很快!

    是說田大有在美國唸過書嗎?還是說有強化學習英語能力的方法能分享呢?
    最近一直在想,如果要辦大場一點的活動,勢必也要有對應國際化的能力,這塊還有很大的進步空間Orz

    btw, 你們家除了「CODE IS POETRY」是常見,還有「Silence is golden.」藏在檔案開頭註解的,也有這件衣服嗎?XD

      1. 哇 原來如此,實在好厲害R! 這學習資訊真讚~ 筆記一下,感謝大大分享XD

        常看你拿到 WordPress 周邊小物,其實亞洲區推廣大使就是你吧!哈哈

        不過用買的好像少了什麼,靠努力,想辦法凹到來 A_A

        (首先從貼紙開始 (誤

發表留言

這個網站採用 Akismet 服務減少垃圾留言。進一步了解 Akismet 如何處理網站訪客的留言資料