用 Ruby 寫碼為例, 說明思考和寫程式同步的樂趣

看到 Thinker 的這噗提到他最近寫程式, 關於思考順序和寫程式順序的影響, 讓我想到以前剛寫 Ruby 時, 發覺思考和寫程式的順序竟然能夠一致, 這種情況下, 寫起程式相當順手。剛換到 Python 時, 反而不怎麼習慣, 花了點時間調適。

以 Thinker 舉的例子為例, 「每行有兩個數字, 將它們相乘後, 加總各行的結果」, 試寫 Ruby 的程式如下:
>> $data = ["2 3", "4 5"]
=> ["2 3", "4 5"]
>> $data.map{ |line| line.split }
=> [["2", "3"], ["4", "5"]]
>> $data.map{ |line| line.split }.map{ |xs| xs.map{ |x| x.to_i } }
=> [[2, 3], [4, 5]]
>> $data.map{ |line| line.split }.map{ |xs| xs.map{ |x| x.to_i } }.map{ |x,y| x*y }
=> [6, 20]
>> $data.map{ |line| line.split }.map{ |xs| xs.map{ |x| x.to_i } }.map{ |x,y| x*y }.reduce { |sum,n| sum + n }
=> 26

懶得裝 Ruby/irb 的人可以用 tryruby.org 試試, 上面的程式碼是我在上面寫好後, 排版後貼過來的。習慣這種資料轉換的思維後 (functional programming?), 配合 Ruby 的 code block, 可以一直線地寫到底, 相當痛快。不過是否好讀, 和習慣和個人喜好有觀, 不在此篇的討論範圍。我自己是覺得寫起來很爽, 但不好讀, 少了用變數抽象化前面操作的結果, 得讀完全部細節才知道在做什麼。

2011-04-15 更新

避免沒寫過 Ruby 的人誤會 code block 像是塞成一行程式碼寫爽的功能, 將最後的結果排版了一下:

>> $data = ["2 3", "4 5"]
>> $data.map { |line|
..   line.split
.. }.map { |xs|
..   xs.map { |x|
..      x.to_i
..   }
.. }.map { |x, y|
..   x * y
.. }.reduce { |sum, n|
..   sum + n
.. }
=> 26

實際寫的時候, 常會要在一個 code block 寫個兩三行, 這時就排版拆行寫了。

留言

  1. 作者已經移除這則留言。

    回覆刪除
  2. 其實Python 也可以寫成一行啦

    thinker_func=lambda data: sum([ int(i[0])*int(i[1]) for i in [ line.split() for line in data.strip().split('\n')]])

    當我們用 for-loop 寫程式時, 基本上我們要考慮到 pre-condition, loop-content, post-condition 的正確性與一致性. 但是因為 list (list comprehension) 的強大, 所以在設計時可以很容易的 "遷就" 上一個流程方塊裡的內容, 所以很適合拿來做方塊之間傳遞資料的容器. 或者說是一種 decoupling, 讓每一行基本上是獨立的 (I/O 都是 list). 我是覺得, 這才是用"回推法"寫程式的有效益的主因. 但是如果是這樣由上往下寫應該也是差不多的.

    回覆刪除
  3. 這裡想強調的不是「可以寫成一行」, 而是思考順序和 coding 順序一致。我自己用 Python 寫的時候, 若要由前往後寫, 變成先寫最裡面的程式 (data.strip().split('\n')) 再回頭寫上一層函式呼叫。

    於是游標得來來回回, 而不是一邊想, 一邊順著思緒往下寫, 不用移動游標。

    code block 的好處是, 它可以放 statement, 介於 imperative 的寫法和 FP 的形式之間, 外層 (code block) 是借用 FP 的概念, 看起來像一連串資料型別轉換; 內層卻可以有像 imperative 一樣有 state, 符合未受嚴謹 FP 訓練的程式設計師的習慣。

    回覆刪除
  4. 我知道你不是想要強調「可以寫成一行」.

    我想說的是, 就像是 thinker 的 code 一樣, 順著寫一樣可以不用回頭去修改. 只要在寫的時候,盡量把東西放到 list 裡去就好. 我從上往下寫一樣會寫出 thinker 那樣的 code. 而且也不需要回頭去改.

    bag=[ line for line in data.strip().split('\n')]
    bag=[ line.split() for line in bag]
    sum([ int(i[0])*int(i[1]) for i in bag])

    所以我不覺得是因為回推法的幫助, 而是因為 list comprehension 這個工具(syntax sugar?), 讓程式更好寫了. 實際上, 這個工具只是讓一個迴圈可以做完的東西, 變成三個迴圈才做完. 但是好處是 input 和 output 是一致的資料結構 (list) 所以在銜接上更簡潔. 這點或許 Ruby 的 code block 做得更好, 這我就不清楚了. 但我猜測往下寫的快感上, 差距可能不是太大.

    我比較贊同你把 thinker 的這個做法和 FP 類比. 我也基於同樣的原因而不認為 thinker 的做法特別. Ruby 和 Python 都大量跟 FP 學習. Python 也有 map/reduce/filter 等基礎函式. 我覺得這種類似的做法, 比較接近 pythonic, 是 Python 語言發展作者群所希望引導學習使用 python 語言的人的寫作方式.

    我以前一直覺得這種 line interpreter 超難用, 很難往回改! 但是當慢慢學習用這種方式寫作時, 才覺得比較適應, 或許這也是她們當初設計時想要克服的困難之一?

    回覆刪除
  5. 1.
    "line interpreter" 是指 interactive interpreter? 像是 irb、ipython 這類工具?

    在 ipython 裡可以打 edit 進入自己慣用的 editor, 寫個 code 再存檔離開, 可降低你說的困擾。通常我得寫較多行時, 會進 editor 寫個 function 再出來。

    2.
    看到你的描述, 讓我又想得更清楚一些了。

    以 FP 的思維來說, 關鍵在於 data type 的轉換。所以, 更精確的說, 差異在 [str] -> [(str, str)] -> [(int, int)] -> [int] -> int, 思考時有個明確的 data type 轉換的流程。如同你說的, list comprehension/code block 有助於用這種思維寫程式。

    所以在和 Thinker 討論後, 我才在這裡想通關鍵在於 reduction (data type 轉換) 的思考方式

    我覺得大家依自己的經驗各自有些想法, 互相討論後似乎愈聊愈清楚了。之後再重制其他人嘗試的經驗, 應該會有更完整的想法。

    3.
    這是比較細的事, code block 的好處是, 可以一路寫到底不用動游標。至少我自己在用 Python 的 list comprehension 時, 有時得先寫 for 後面的東西, 再移回來寫 for 之前的東西, 而覺得有一點卡

    避免落入 Ruby vs. Python 的討論, 先聲明我個人是偏好用 Python 開發程式 :D

    回覆刪除

張貼留言

這個網誌中的熱門文章

(C/C++ ) 如何在 Linux 上使用自行編譯的第三方函式庫

熟悉系統工具好處多多

virtualbox 使用 USB 裝置