不,我不是在說 Wittgenstein 的理論。
thinker 寫了一篇很有趣的文章
心靈與程式碼的協奏曲. 也引發了我的一些想法. 不過我 coding 功力完全比不上 thinker. 以下只是我的一些個人感想而已.
假設問題是, function 接受一字串,裡面的每一行有兩個欄位,皆是整數。欄位以一個空白相隔。 function 必把每一行的兩個欄位的數字相乘,然後再將每一行的結果加總。
雖然 thinker 強調這種回推法或者Top-Down的設計方式的不同, 但是我倒覺得這並不是設計方法的不同而是工具的不同, 而且即使採用回推法, 如果之前的設計不好, 最後還是要回去修改原來的程式, 這跟傳統的做法沒有太大的差別. 以這個例子來說, thinker 一開始想到的是,
result = sum(value_of_lines)
但是這裡其實就是一個有趣的地方, 為什麼 values 會被存放在 list 裡面? 原因不就是因為 Python 大量的建議使用 list 作為基本的資料結構? 我基本上還是比較習慣一行一行往下寫的做法, 但是因為工具 (list comprehension) 的關係, 我想到的程式是
001 def mul_n_sum1(data):
002 bag=[ line.split() for line in data.strip().split('\n')]
003 result=0
004 for i in [ int(i[0])*int(i[1]) for i in bag]:
005 result+=i
006 return result
和 thinker 的程式相去無幾了. 更精簡一點, 我可能寫成這樣.
001 def mul_n_sum2(data):
002 bag=[ line.split() for line in data.strip().split('\n')]
003 return sum([ int(i[0])*int(i[1]) for i in bag])
以我寫的程式來說, 兩種風格都有, 決定會用 for-loop 或者 list comprehension 基本上是依據
- 有沒有我已知的方便的函數
- Data 的大小
- for-loop 裡內容的複雜度
- 有沒有想把程式寫短一點的慾望
第一點是非常重要的. mul_n_sum1 的怪模樣就是因為我一時忘記了 sum() 的存在 XD 所以這個情況下如果我硬要寫短, 可能會寫成這樣 XD
001 def mul_n_sum3(data):
002 bag=[ line.split() for line in data.strip().split('\n')]
003 return reduce(lambda x,i: x+int(i[0])*int(i[1]), bag,0)
第二和第三點當然也非常重要, 我處理的資料通常是幾GB的大小, 我不太會在這種情況下採用 list comprehension.
對我來說, 一般而言, 寫程式比較接近填空, 先搞清楚自己要做什麼事, 然後大概幻想一下流程 (如果之前寫過類似的程式,流程圖會很快出現), 然後把流程圖裡的方塊一個一個填進去並實作出來. 由於 Python 本來就想要加入 functional programming 的特色, 而且 list 又是對 Python 來說最自然的資料結構, 用 list 串起每個方塊是很自然的想法, 因此寫出類似的東西, 或者說有點 FP 風格的 code 並不奇怪.
當 thinker 一開始決定用
result = sum(value_of_lines)
來實作時, 其實已經限制了接下來的回推要怎麼做了. 如果今天是用C語言的話, 往上回推的寫法大概還是出現 function, recursive function, for-loop 幾種寫法吧. 因為自己實作 dynamic list/array 可能還比較麻煩XD 如果是要實作迴圈來加總, 其實又會碰到一樣的問題了.
我假設自己在C裡面用倒推法寫程式, 第一步是
result = sum(value_of_lines)
然後我們就要設計 value_of_lines 是怎麼來的, 如果是在 C 語言裡, 我直覺有兩種做法
value_of_lines=multiple_pairs(two_D_array)
或者
value_of_lines=multiple_two_vector(column_1,column_2)
可以想像, 這兩種函式不論怎麼寫, 大概都要用到 for-loop/recursive. 所以下一步,我們應該思考什麼? 如果不考慮細節, 這個時候我們要開始設計資料如何存入對應的資料結構. 如果是方法一, 大概就是如下
two_D_array=parse_data_line_by_line(data)
如果是方法二, 就比較麻煩了, 我們必須一次寫兩行,
column_1=parse_data_line_by_line_and_fetch_column(data,1)
column_2=parse_data_line_by_line_and_fetch_column(data,2)
或者多幾個步驟
struct_of_two_vector=parse_data_line_by_line(data)
column_1=struct_of_two_vector->column_1
column_2=struct_of_two_vector->column_2
顯然麻煩多了. 而且繼續寫之後會發現, 我們需要自定一些資料結構, 才能夠保存 array 的長度的資訊. 否則就得 "回頭" 去改函式的介面.
但通常我們不會選擇這條路徑. 因為在寫當下的程式之前, 我們已經稍微往前設想到資料的特性 --- data 被 parse 出來大概長怎樣.
如果用 Python 做這樣的動作, 產生的程式碼大概如下
001 def mul_n_sum(data):
002 txt_field_lines=[ line for line in data]
003 txt_field1 = [ int(line.strip('\n').split()[0]) for line in txt_field_lines]
004 txt_field2 = [ int(line.strip('\n').split()[1]) for line in txt_field_lines]
005 value_of_lines = [ txt_field1[i]*txt_field2[i] for i in xrange(len(txt_field1))]
006 result = sum(value_of_lines)
007 return result
雖然比之前的程式沒有複雜太多, 但是我寫起來可一點都不覺得順暢XD 而差異僅僅只是源自 "資料來自於兩個 lists 或者 一個 list"
當我們用 for-loop 寫程式時, 基本上我們要考慮到 pre-condition, loop-content, post-condition 的正確性. 因為 list 的強大, 所以在設計時可以很容易的 "遷就" 上一個流程方塊裡的內容, 所以很適合拿來做方塊之間傳遞資料的容器. 我是覺得, 這才是用"回推法"寫程式的有效益的主因.
附帶一提,原始命題也暗示了處理的方式,如果說原來的題目描述如下,或許讓人想要用兩個向量相乘的作法。
假設問題是, function 接受一文字檔,裡面有兩直行,每一直行代表一整數向量,欄位以一個空白相隔。 function 求出兩向量之內積。
Ref.: fcamel 技術隨手記:
用 Ruby 寫碼為例, 說明思考和寫程式同步的樂趣