2014年5月10日 星期六

從不同程式語言的異同思考問題的本質

《學程式語言的樂趣》描述我早期學程式語言的事,後來幾年多了一些心得 ( Python, Haskell, C++, Objective-C ),之後有機會再補上。這裡想寫的是藉由學習不同程式語言後明白的事。

學特別的程式語言有助於掌握不同的概念。我最初慣用的語言是 C (高中三年),再來是 Java (大學四年)。藉由 Java 開始接觸 OOP 的思維,除了程式語法之外,學到更多的是寫程式的思考方式。其實回頭再去看 C,也可以使用一樣的思維,只是當語言直接綁入思維時,會被迫這麼做。就像用 Haskell 必須寫出 stateless 的程式,就會更明確地留意 state。不過學 Java 的時候還沒察覺到這點。

後來長時間地陸續用了 Ruby、Python、C++,還有加減寫 JavaScript。開始掌握到一些程式語言背後共同的特性:組合邏輯控制,以及管理資料的狀態。回歸到最原始的電路來看,程式的運作就是邏輯控制 (and/or) 和狀態 (on/off)。當要做的事變複雜時,必須結構性地管理它們。

舉例來說:

  • Java 定了 class 和 interface 表示「資料+實作邏輯」和「操作介面」,class 內又有 public/(default)/protected/private 限制資料的存取範圍。還有 package 封裝同一群 class。
  • Python 表現的比較直接,容易連結回 C 該如何表現這些思維。每個 method 第一個參數一定是物件本身,慣例命名為 self。'_' 開頭的 function 和 method 表示只限於內部使用。用 module 封裝同一群 class。
  • C++ 藉由不同的方式使用 virtual,class 可當作 "class" 或當作 "interface",或帶有實作的 "interface"。class 有 public/protected/private 限制資料的存取範圍,但又有 friend 可讓指定的 class 存取無視這些限制。有 namespace 封裝同一群 class,還有 anonymous namespace 可以嚴格的封裝實作細節。

不同的語言有各自擅長的地方,學習別的語言的時候,會想要借鏡別的語言的特色,運用到原本常用的語言裡。但是接觸過 Scheme 和 Haskell 再回頭嘗試用在 Python 和 Java 的時候,遇到很大的問題:

  • 無法直覺地使用天生不支援的功能。比方說 Java 的 function 不是 first-class function,得用物件包一層,再自成一套體系。例如用 functionaljava
  • 和原本語言本身的風格不合,增加其他人的維護成本。

第二點是比較大的問題。自己用了一套「functional Java」的寫法,其他人也得學習如何解讀。以 Python 舉例,下面是同一件事用三種不同寫法:

for-loop

numbers = []
for i in range(0, 10):
    numbers.append(i * 2)

list comprehension

numbers = [i * 2 for i in range(0, 10)]

map

numbers = map(lambda i: i * 2, range(0, 10))

對沒學過 Python 的人來說,for-loop 比較直覺,沒學過 Python 也可推敲出意思;初看 list comprehension 不明白這是什麼,不過花點時間看幾個例子就懂了。map 比較困難,得有 functional programming (FP) 的概念才能明白。

Python 本來就有這些語法,我們不會責怪這樣寫的人。但用在 Java 裡就有爭議了,為什麼要另外包一套框架,寫出要別人花額外工夫才能讀懂的程式?

後來我察覺到更深層的思維:OOP 也好、FP 也好,目的都是結構性地管理資料和邏輯,只是表達的方式不同。掌握到這點後,我將時間花在有助於真正目的事上,而不是寫得更 OOP 或 FP。原本遇到的困擾 (如何在 B 語言裡善用 A 語言的特性) 自然就不存在了。

舉例來說:

  • 為什麼 FP 要強調 immutable,還有習慣用 map? 因為 immutable 的資料沒有 state,自然沒有被誤改的風險。有沒有用 FP 的 map 到不是重點,map 只是方便套用一套控制邏輯產生另一批 immutable 資料。是實作層級的小幫手,不是設計準則。
  • 為什麼 OOP 強調 Tell, Don't Ask? 也是為了減少外漏 state。即使用沒有 OOP 的語言,也可這麼做。像 iOS 的 Quartz 2D,API 需要用的 CGContextRef 是 struct CGContext* 的 typedef,但是使用者不知道 struct CGContext 是什麼,只知道呼叫函式時要傳入 CGContextRefQuartz 2D 封裝需要的 state 在 struct CGContext 內,隨時可改變它的結構。
  • 為了方便模組之間獨立開發,不用擔心日後改寫模組內部的實作造成外部程式出錯,最重要的是定出乾淨的介面,讓別人不會誤用。再來才是善用語言的特性讓別人想誤用也不行。例如 C++ 的 anonymous namespace 是隱藏實作的好方法;Java 的 package + (default) class 也是好方法;Python ... 只能用底線命名然後祈禱不要有人亂搞。

從問題的本質 (管理資料和邏輯) 來看,自然會明白這些設計準則,還有區分出設計和實作 (準則和表現方式)。剩下的是衍生想法,像是管理 state 方法的優先順序:

  • 沒有 member field (沒 state 最好!)
  • 有 immutable member field (非得有 state 不可的話,至少讓它不可修改)
  • private mutable member field (非改不可的話,盡可能減少能改它的程式碼)
  • ...

重點還是掌握問題的本質,避免迷失在解決的方案之中。

結語

總結來說,一套程式語言有一套思維,試過多套不同特性的語言後,藉由它們之間的同異處,可以察覺這些語言想解決的問題是什麼。繼而明白真正的問題是什麼,再回饋到解決問題的思維中,避免落入特定解決模式裡 (如道地的 OOP 或 FP)。起初學不同語言只是覺得好玩,誤打誤撞得到這樣的體會,滿有意思的。

相關文章