2011年7月31日 星期日

semantic versioning

WanCW前篇留言 提到 《Semantic Versioning》, 是 github 的創辦人 Tom Preston-Werner 寫的。當初好像就是看到這篇吧。

文中先說明為了減少 dependency hell 的影響, 若大家定版號時共同遵守同樣的規範, 就能比較放心地說明自己的 package 需要那些版本的 packages:

If the dependency specifications are too tight, you are in danger of version lock (the inability to upgrade a package without having to release new versions of every dependent package). If dependencies are specified too loosely, you will inevitably be bitten by version promiscuity (assuming compatibility with more future versions than is reasonable). Dependency hell is where you are when version lock and/or version promiscuity prevent you from easily and safely moving your project forward.

接著提出 semantic version 的定義:

Consider a version format of X.Y.Z (Major.Minor.Patch). Bug fixes not affecting the API increment the patch version, backwards compatible API additions/changes increment the minor version, and backwards incompatible API changes increment the major version.

然後用嚴僅的方式陳列 semantic version 的定義, 言簡意賅。這裡用我的方式描述一下, 完整的定義以原文為準:

  • 版號 X.Y.Z 代表 Major.Minor.Patch
  • X = 0 時, 隨便你搞
  • X > 0 後, 在程式裡或文件註明那些是 public API
  • 沒動到 public API (如修 bug、重構):Z++
  • 動到 public API 相關程式, 但介面不變 (如加新功能):Y++
  • public API 介面有變:X++

FAQ 裡有說明如何處理特殊情況, 像是「若一有 incompatible API changes 就升 major version, 那不是一下就跳到 42.x.x」? 作者回答那表示你沒有認真看待 public API, 要盡可能地減少 incompatible changes。「若不小心破壞規則, 該如何修正」? 作者回答修正回來後, 再發一個新版號, 並在文件中註明沒遵守規則的版號。另外還有一個 issuegithub 上, 供大家反應不完備的部份, 真是不錯的規範。

2011年7月30日 星期六

Python 2 to 3 的問題

看到有朋友在問 python 2 和 python 3 有何差距, 來寫一下自己的理解。先聲明, 我沒有實際轉過。

之前曾在別的地方讀到版本編號的含意, 忘了出處, 找了一下發現 RubyGems 的說明和我先前看到的內容一樣, 對版本 a.b.c 來說:

  • a 往上 +1, 表示有大幅改版, 會有不向下相容的部份, 官方 release note (或 changelog) 裡會描述那些地方沒有向下相容 (backwards incompatible changes), 如 Fabric 的 Changelog 有提到那些函式行為和之前不同。
  • b 往上 +1, 表示有曾加新 feature 或 bug fix, 但介面沒有改變, 可以安心昇級
  • c 只是修改實作方式, 對使用者來說, 行為完全沒變。

當然還有許多其它的表示方式, 就.....先忽略它們好了。說明這點是想強調 2 到 3 的難處, 在於有些不相容的改變。

《Case study: porting chardet to Python 3 - Dive Into Python 3》提供一個轉換的案例, 其中 Fixing What 2to3 Can’t 說明要手動修改的部份。

除了修改自己程式的問題外, 但是若別人的程式沒有升到 3, 改完也沒用。比方說大家都愛用的 NumPy, 官方說目前只支援 2.4 ~ 2.6, 像 SciPymatplotlib (強大的繪圖函式庫) 會用到 NumPy, 在 NumPy 升上去以前, 它們也無法升級。所以在重量級 packages 支援 python 3 以前, 一般使用者仍不方便升到 python 3。

2011-07-30 Updated

看到 Python 3 Wall of Shame 有統計各 package 被下載的次數以及是否支援 python 3, 才發覺原來 NumPy 已支援 Python 3 了

2011-07-31 Updated

WanCWEric Chen 提醒, 更新如下:
* 版號的定義是寫在 Semantic versioning
* matplotlib 已有官方的 Python3 branch

2011-08-02 Updated

Eric Chen 提醒, SciPy 也有支援 3.x

2011年7月29日 星期五

JavaScript 的 prototype

以下將我目前理解的內容寫下來, 有錯還請指正。

概念

翻了數篇說明文後, 發覺任何嘗試用 class-based OOPL 來解釋 prototype 的文章, 剛開始看起來滿合理的, 最後都會愈解釋愈複雜, 並且產生矛盾。這或許和大部份人根深蒂固認為 「 使用 (class-based) OOP == 易於維護的程式碼」一樣, 是難以破除的迷思, 總會想用 class-based OO 來解讀其它語言。個人認為這和學校老師和坊間一堆入門書有很大的關係。直接理解 prototype 的設計理念和它對應的用法, 會比較順。

若有耐性的話, 看完《Details of the object model》, 就會清楚 prototype 為何。不過要留意文中關於 class-based OOPL 的描述有些不正確, 像 Ruby、Python 這類 dynamic typing 的語言, 定義完 class 後仍能修改 class 的屬性。沒耐性的話, 可以看較簡短的說明: 《JavaScript 的 prototype(上)》《以 C 語言實做 Javascript 的 prototype 特性》

用我自己的說法, JavaScript 的 OO 特性為:

  • 所有物件都是一樣的, 只有 "instance", 沒有 class 和 instance 之分 (反之, 就得花費不少篇幅說明 class 是 "Class" 的 instance)。
  • 物件之間的關係透過 prototype chain 串起來: 每個物件都有它的 prototype, 自己找不到屬性時, 就往 prototype 找。
  • 使用 closure 封裝 scope。待理解得比較深之後, 再另寫篇文章說明。

prototype chain 的效果

從下面這段程式可看出它的效果:

function X() {}
function Y() {}
X.prototype = new Y;
x = new X;
console.log(x.a); // undefined
Y.prototype.a = "yy";
console.log(x.a); // "yy"; x.a -> X.prototype.a -> Y.prototype.a
X.prototype.a = "xx";
console.log(x.a); // "xx"; x.a -> X.prototype.a
x.a = "aa";
console.log(x.a); // "aa"; x.a

接著這段程式在 new 完物件後, 換掉 prototype, 藉此突顯 prototype chain 是順著物件的 __proto__ 找, 而不是找 X.prototype 指到的物件。

function X() {}
x = new X();
console.log(x instanceof X); // true
console.log(x.a); // undefined
X.prototype.a = 3;
console.log(x.a); // 3
X_original_prototype = X.prototype;
function Y() {}
X.prototype = new Y; // 換掉 prototype
console.log(X.prototype.a); // undefined
console.log(x instanceof X); // false
console.log(x.a); // 3
X_original_prototype.a = 5;
console.log(x.a); // 5
X.prototype = X_original_prototype;
console.log(x instanceof X); // true

Foo() 和 new Foo() 的差異

function Foo() {
    this.a = 3;
    return 9;
}
x = Foo();
console.log(x); // 9
x = new Foo();
console.log(x); // Foo { a=3 }

總結

  • 對 function Foo 來說, x = Foo() 和 x = new Foo() 的行為截然不同。前者是呼函式取得回傳值; 後者不會取得回傳值, 而是產生一個新的物件, 並將 x.__proto__ 指向 Foo.prototype。
  • __proto__" 是 JavaScript interpreter 內部記錄 prototype 的物件, 不是正式的 JavaScript 定義
  • 執行 function Foo() {} 後, 會產生物件 Foo:
    • typeof(Foo) === "function"
    • 產生 Foo.constructor, 其值是一個函式, 即 "function Foo() {}"
    • 產生 Foo.prototype, 可被換為其它物件
  • foo = new Foo() 會產生物件 foo:
  • 一般的 function call (少了 new) 只會得到回傳值, 不會串起 prototype chain
  • JavaScript 沒有區分 method 和 field, 都視作 property
  • foo.x 會先查自己是否有這個 x 這個屬性。沒有的話, 再往 foo.__proto__ 找; 還是沒有的話, 再往 foo.__proto__.__proto__ 找, 直到沒有 __proto__ 為止 (即 Object.prototype)

習慣 prototype 的思維後, 我覺得這個設計還挺漂亮的, 透過一致且簡單的規則, 可以彈性地加減共通屬性。

2011年7月28日 星期四

python debug 入門

記得剛學寫 C 時, Scott 一再告誡我們不要依賴 printf 或 debugger, 要自己能掌握全局, 想清楚整個邏輯是怎麼一回事再動手。矯枉過正的結果, 就是幾乎沒什麼在用 debugger。偶而用一下不順, 又放棄使用, 覺得還是配合 logger、讀通原始碼和 unit test, 才是正規解法。不過有時候還是用 debugger 比較省事, 最近開始加減練習用 debugger 來 trace code。

pdb 入門方式滿簡單的, 兩種執行方法:

  • python -m pdb YOUR_MODULE
  • 在 YOUR_MODULE 開頭寫
import pdb
pdb.set_trace()

這篇用一個簡單的例子說明 pdb 有多方便, 一邊執行一邊觀察, 還可以順便試看看怎麼改才對, 再繼續跑, 看即時寫入的修正碼是否有效。反正是 interpreter, 可在執行期間隨意插入程式碼。

這篇說明常用指令, 比官方死版版的文件清楚多了。

若希望看到彩色的輸出, 可以用 ipdb, 站在 ipython 的肩膀上, ipython 有的好處, ipdb 都有!! 不過 python -m ipdb YOUR_MODULE 的用法, 只有在 python2.7 開始才有效。

《CPython 源碼剖析》讀書心得 - ch1 - python object

固定的記憶體空間

object 初始化後大小不準變動, 這個限制帶來一個好處: 大小不變表示不可能要求更大的記憶體空間; 不要求更大的記憶體空間表示不可能要求重配一塊區域, 因此也不會改變 address, 所以, 不用擔心之後要更改任何 pointer 的值。

這讓我想到 MySQL InnoDB 的設計採用 clustered index 存資料, clustered index 裡的 index columns = primary key (PK) 接著針對其它 index (non-clustered index), MySQL 改存 PK 的值, 而不是存 data table 的 physical location (MyISAM 的作法)。

這個設計帶來兩個影響

  • 優點: data row 改變位置時 (B-Tree 常分割或合併 node), 不用更新 indexes
  • 缺點: indexes (非 PK) 得先找到 PK, 再用 PK 取資料, 多了一步

不過 MySQL 針對缺點又做了 InnoDB cache, 讓常取的資料能一步就取到資料, 但也增加維護成本, 這是另一個議題了。

PyObject

C 裡沒有 OO 的語法, Python 用 #ifdef 來增加選擇性欄位, 用 #define 擴充欄位並保留同樣的 address offset。所有 object 的 struct 開頭都是

typedef struct {
   PyObject_HEAD
   ...
} PyXObject;

PyObject_HEAD 是巨集, 最簡單的定義如下:

#define PyObject_HEAD \\\\
   int ob_refcnt;
   struct _typeobject *ob_type;

另有 PyObject_VAR_HEAD 幫動態的物件加上 ob_size 欄位:

#define PyObject_VAR_HEAD \\\\
   PyObject_HEAD
   int ob_size;

所以, 最基本的 python object 和含有不等量的 object 定義如下:

typedef struct {
   PyObject_HEAD
} PyObject;

typedef struct {
   PyObject_VAR_HEAD
} PyVarObject;

類別 int 的定義如下:

typedef struct {
   PyObject_HEAD
   long ob_ival;
} PyIntObject;

這個作法漂亮的地方在於, 彈性地定義不同類型物件用到的欄位, 又能讓所有物件有共通的開頭欄位。於是, PyObject* 可以指向任何物件, 並能放心地假設:

  • 第一個欄位是 ob_refcnt
  • 第二個欄位是 ob_type

若用 PyVarObvject*, 則能再假設

  • 第三個欄位是 ob_size

以基本型別為例:

intfloatstringlist
ob_refcntob_refcntob_refcntob_refcnt
ob_typeob_typeob_typeob_type
ob_ivalob_fvalob_sizeob_size
ob_shashob_item
......

2011年7月27日 星期三

《CPython 源碼剖析》讀書心得 - ch1 - bytecode

複習一下過年時讀的筆記, 若妄想等我全讀完再整理成文章, 大概這輩子都不會有結果吧。還是讀個一個段落就隨手寫寫, 比較實際。我相當地不熟這個領域, 有錯還請指正。

ch1 開頭只提了一點, 剛好幫我串起了之前的一些記憶。想到其實任何語言都可以自己搞個 VM 和 bytecode,
接著在針對各種硬體實作 VM, 就能跨平台了,
當然, 那些用到硬體相關 函式庫/指令 的程式碼, 就無法跨平台

所以, JVM 之所在在跨平台這點上被特別提及,
主要應該是 JIT 讓 java bytecode 執行得很有效率,
且主要的硬體裝置都有支援, 才會覺得很威。而容易實作 JIT 的原因, 和 bytecode 的設計有很大關聯,
像 JVM 針對 stack machine 不用 register 就省掉不少工
( 但 Anroid 用的 Dalvik 為了快, 又加回使用 register 的設計 )。

而其它語言一開始沒規劃好 bytecode 轉 native code 這段,
後期要補就比較辛苦:

  • 像 Unladen Swallow 想 100% 支援 Python 3 的 CPython,
    得在既有的 python object 上多加其它欄位存 JIT 相關資訊,
    結果是速度有可能變快, 但占用的記憶體空間會變更大
  • PyPy 沒要求 100% 相容 Python, 也沒要求相容 CPython, 做起來省了不少工, 較易發揮
  • JavaScript 的 V8 雖然很威, 也有不少限制, 像是它沒實作 multi-thread, 在瀏覽器端不成問題, 換到平常的應用時, 就會有點綁手綁腳的。《Is node.js best for Comet?提到Plurk 用 node.js 卻因 gc 也在 main process 裡執行, 造成的效率問題。

ps. Unladen Swallow 和 PyPy 是很久以前和 Scott 聊到的, 我沒深入看相關文件, 可能會有誤解。另外, 半年前寫這份筆記後沒多久, 就看到《Unladen Swallow Retrospective》, 科技變遷的太快, 令人不勝唏噓。還沒開始讀相關的文獻, 就聽到它掛掉的消息...。

2011年7月17日 星期日

[].slice.apply(...) 的作用

偶然看到這樣的寫法:

[].slice.apply(document.images).filter(is_gif_image).map(freeze_gif);

想說這是啥鬼東西, 後面的 filter、map 不難懂, 但前面的 [].slice.apply 在做什麼? google 一下找到這篇說明, 原來是將非 Array 的物件轉為 Array, 方便使用 Array 的其它方法, 如 filter 和 map。對應 python 的思維的話, [].slice.apply(something) 類似 list(iterator) 吧。

2011年7月14日 星期四

解決 iPython 無法輸入中文的問題

經同事 P 分享這篇, 才知道解法。實測後發覺註解掉 /.ipython/ipythonrc 的 readline_parse_and_bind "\M-i": " " 即可。問題出在 unicode "\u9???" 的文字, 也就是 utf-8 的 "\xe9\x??\x??"。我在 Ubuntu 上試著按 Atl+i, 發覺出來的是兩個字元: 先 ESC 後 i。不懂為什麼這會和 \xe9 沖到。

觀察 ipython 的原始碼, 發覺用到 Python的 readline, 但這個模組是由 C 實作的, 就沒再追下去了。還沒弄清楚怎麼修改並重編 C 相關函式庫, 改天有類似需求時再來試試。

Effective Java 讀書筆記: Item 50 - 考慮用更適合的型別替代 String

原本不覺得這則有什麼特別的, 翻 Head First - OOAD 後, 看到裡面第一個例子就是將一堆屬性的型別從 String 盡量換成 enum, 程式瞬間變清楚許多, 而且也可避免大小寫或拼錯字的問題, 字串比對有點囉唆, 而且 enum 的比對效率也比 String 快 (因為 enum 的每一元素都是 singlton, 可用 == 直接比對)。才想到之前也看過類似的小問題, 只是量不大, 只覺得程式並不清楚, 到沒有沒特別嚴重, 結果沒想到要去重構它們。

附上 Head First - OOAD 的例子, 看了比較有感覺。修改之前:

Guitar:
----------------------------
serialNumber: String
price: String
builder String
model: String
type: String
backWood: String
topWood: String

修改之後:

Guitar:
----------------------------
serialNumber: String
price: double
builder Builder
model: String
type: Type
backWood: Wood
topWood: Wood

上面的 Builder、Type、Wood 是 enum。這樣在提供搜尋吉它的介面時, 可以省掉一些前處理 (像是 toLower()、trim() ) 等操作。

2011年7月13日 星期三

Effective Java 讀書筆記: Item 52 - 用 interface 宣告變數

這是一則順手做不花力氣, 但不做也無傷大雅的建議。

作者建議這麼寫:

List<String> names = new ArrayList<String>();

而非這麼寫:

ArrayList<String> names = new ArrayList<String>();

這樣若要替換實作方式時, 只需改一行程式即可。比方說從 single-thread 要改為 multi-thread, 只要改變產生 names 的程式:

List<String> names = new Vector<String>();

剩下用到 names 的程式都不用改。

以前我覺得這是理所當然該做的事, 甚至用這做為「是否具備 Java 常識」的指標之一。後來發覺實在是太多人不知道這個準則, 而開始重新思考究竟沒做到這件事會有什麼後果?

結論是: 其實不太嚴重, 一來這鮮少發生。二來若真有那麼一天需要換實作, 配合 IDE 也可以很快地改完 (static typing 萬歲!!)。雖說不遵守這個準則也無傷大雅, 我仍覺得照著做較好。如同之前在《養成寫程式的好習慣》所言, 做好許多這類小細節, 長久下來會少掉很多問題, 這類的成本是難以追蹤的。

2011年7月12日 星期二

在 Ubuntu 取得 libc 的 source code

$ sudo aptitude install dpkg-dev
$ apt-get source libc6

Effective Java 讀書筆記: Item 38 - 注意串接字串的效率

最近發覺不少人沒注意到這件事, 寫一下心得。

Joel 有寫一篇很長的描述, 說明串接字串的效率問題, 有興趣的話值得一讀。結論是, 若一直用 "+=" 的寫法 (或是 C 的 strcat ) 串接 N 個字串, 效率是 O(N^2)。Java 有提供 class StringBuffer 和 StringBuilder 提供 O(N) 的實作。做法是每次要 reallocate array 時就要兩倍的空間, 這樣 amortized time 就會是 O(N)。並且最多只浪費一半的空間。

StringBuffer 和 StringBuilder 都是繼承 AbstractStringBuilder, 將主要運算轉包給父類別。兩者的差別是 StringBuffer 的方法有加上 synchronized, 也就是說, StringBuffer 是 thread-safe, 而 StringBuilder 不是。所以, 在使用單一 thread 的情況下 (通常如此), 應該使用 StringBuilder 以獲得較快的時間。

附帶一提, 若目的是 join 的話, 用 Apache Commons LangStringUtils.join() 會比自己用 StringBuilder 刻來得省事。

2011年7月8日 星期五

SQLite 的 on duplicate update

MySQL 的 insert ... on duplicate update 相當方便, 可惜不是標準語法。參考這篇得知, SQLite 有兩種相關語法:

  1. insert or ignore into ...
  2. insert or update into ...

第一個語法和 MySQL 的 insert ignore 一樣, 而第二個的行為有所不同, 它會先 delete 再 insert。所以若有設 primary key 為 auto increment, 執行 insert or update into 後, primary key 會變。

所以該篇文章提供的作法是:

INSERT OR IGNORE INTO visits VALUES ($ip, 0);
UPDATE visits SET hits = hits + 1 WHERE ip LIKE $ip;

這樣的好處是

  • 邏輯簡單, 不用先 select 再決定要做 insert 或 update
  • update 就是 update, 不是先刪除再 insert, 所以 primary key 不會變

唯一的缺點是不論原本有無資料都要執行兩次 SQL, 但對 SQLite 的應用來說, 這無關緊要。

2011年7月6日 星期三

Effective Java 讀書筆記: Item 38 - 檢查傳入參數

記錄讀書心得, 內容不一定和書上一致, 有些是我自己的看法。

早期發現, 早期治療。傳入參數時就先檢查, 這樣有錯時比較清楚原因為何。

針對方法的存取級別, 行為有所不同:

  • exposed API: 有錯就丟 exception。記得寫清楚註解, 說明會丟那些 exception
  • 內部用的: 用 assert 即可。錯了就讓它掛, 馬上修。外部使用時也可透過 java interpreter ( -ea / -da) 參數決定是否要執行 assert 的程式。不執行的話, 可以提昇速度

Effective Java 讀書筆記: Item 37 - 用 marker interface 定義型別

記錄讀書心得, 內容不一定和書上一致, 有些是我自己的看法。

marker interface 是指沒任何 method 的 interface, 充其量是用來表示一種型別, 有填入 meta data 的意味。但 marker interface 不如 annotations 彈性, 擴充 interface 的 method 意味著強迫所有 client code 要改寫, 而 annotations 無此困擾, 可以再擴充屬性。

有了 annotations 後, marker interface 還是有一點好處: 它可以保證在編譯時找出不當的參數傳遞。比方說若 ObjectOutputStream.writeObject() 的參數定義成 Serializable 而不是 Object 的話, 就能保證不會有人用錯 writeObject(), 傳入的物件一定支援 serialization。相對來說, annotations 得在執行時才能偵測到錯誤, 不如編譯時偵錯來得方便。

仔細想想, 之所以會有 annotations vs. maker interface 的 trade-off, 原因是 Java 為了避免多重繼承衍生的問題, 不支援多重繼承, 改用多重實作代替。但不能為 interface 的 method 提供預設行為, 大幅提高擴充 interface 的成本 (必須改寫所有用到的 class)

參考《侵入,无侵入? Annotation vs Interface》的一些例子, 比較明白有些框架會用 marker interface 或 annotations 表示 class 的屬性, 從而影響框架處理物件的方式。Joshua Bloch 強調: 若在使用 annotations 時用到 ElementType.TYPE (表示這個 annotation 只能用在 class、interface、enum), 多想一想是否用 marker interface 更合適。自己沒遇過實例, 還無法體會, 總之就先備忘吧。

Effective Java 讀書筆記: Item 36 - 善用 @Override

記錄讀書心得, 內容不一定和書上一致, 有些是我自己的看法。

在 method 加 @Override 可確保自己不會手殘沒覆寫到該覆寫的 method。經典的犯錯例子是覆寫 class T 的 equals, 但參數沒有用 Object 而不小心寫成 T, 像是這樣:

public equals(T other) // WRONG

有加 @Override 的話, 會造成 compilation error。盡可能用 @Override, 沒任何害處。

另一個我常用的內建 annotations 是 @Deprecated, 配合 IDE, 可先避免繼續使用不適用的方法 (會有 warning), 再來逐步換掉 caller, 最後再移掉 deprecated method。

2011年7月4日 星期一

Effective Java 讀書筆記: Item 35 - 使用 annotations 替代 naming patterns

記錄讀書心得, 內容不一定和書上一致, 有些是我自己的看法。

1.5 後有了 annotations, 方便將資訊加入 constructor、method、field、local variable 等處。官網有一個簡單的例子, 說明 annotations 的用處; 書上也是用 @Test 為例, 但寫得更清楚, 講比較多例子。

了解 annotations 強大之處最快的方法就是看 JUnit 4 的用法 (只要六十秒!!), 簡單易懂又沒有繼承class 的負擔, 也不用擔心打錯字時 compiler 抓不出錯誤 (像是 test 寫成 tset)。JCommander 也是不錯的例子, 很清楚地表示命令列參數, 滿好用的命令列框架。

btw, 初學 annotations 時, 我不太能抓到它的意思。搞半天才明白它只能填入 meta data, 還要另寫程式使用 reflection 讀出 annotations 才有用處。也就是定好規格, 用 annotations 填入規格的細節資訊, 再用另外的工具讀出 annotations 的內容做對應處理。

乍看之下, Python 的 decorator 語法和 annotations 很像, 但彈性多了: decorator 本身就是操作方式, 而不是 meta data, 方便使用; 再加上 Python 的 function 是 first-class object, 不需使用「reflection」即可操作 function, 方便實作。

2011年7月3日 星期日

Effective Java 讀書筆記: Item 32 - 使用 EnumSet 替代 bit fields

記錄讀書心得, 內容不一定和書上一致, 有些是我自己的看法。

這節說明有 java.util.EnumSet 這種好東西, 不用擔心使用 enum 後就沒有位元運算可用。以往用 int 表示 constant 時, 常會技巧性地將常數設成 2 的次方, 方便之後用位元運算表示常數的聯集、找出交集等。而 EnumSet 針對 enum 實作了相關操作, 底層也是使用 long 或 long[] 表示, 不用擔心時間或空間成本。

有興趣學習位元運算的技巧的話, 看實際實作 EnumSet 的 RegularEnumSet 和 JumboEnumSet 的原始碼可以學到不少東西, 像是用 population count 速算集合裡的個素。

Effective Java 讀書筆記: Item 31 - 透過 instance field 傳入數值, 別用 ordinal 代替

記錄讀書心得, 內容不一定和書上一致, 有些是我自己的看法。

這則和後面幾則的重點之一是別亂用 ordinal 做些隱諱的事, 像是「巧妙地」用 ordinal() 表示用到的數字, 比方說 enum { NONE, SINGLE, COUPLE }, 這樣 ordinal() 剛好可表示人數。相信同意「explicit is better than implicit」的人不會對此有異議。

那 ordinal 能用來幹嘛呢? 作者建議盡可能別用它, 它主要的目的是給 EnumSet 和 EnumMap 這類資料結構使用。

Effective Java 讀書筆記: Item 30 - 善用 enum 表示常數和相關操作

記錄讀書心得, 內容不一定和書上一致, 有些是我自己的看法。

參照官網介紹的例子 enum Planet, 可以清楚地明白使用 enum 的好處。書上列舉的好處有:

  • 安全、易讀、執行速度不輸 int 的實作版
  • 天生 immutable
  • 已實作 equals()、hashCode()、Serializable、Comparable

Making the Most of Java 5.0: Enum Tricks 這篇進一步說明使用 enum 的技巧:

  • reverse lookup。key 可以是 code 或字串。enum 會自動生成 valueOf(String) 提供字串的反查, 但有覆寫 toString() 的話, valueOf() 就失效了。這時記得提供自己寫的 fromtString() (不能覆寫 valueOf(), 因為 enum 的方法都是 final)
  • template method。書上稱這個為「constant-specific method implementations」, 重點是藉由 abstract method 保證之後新增的欄位不會漏實作必要的 method。

這則後半都在討論如何寫出好程式, 讓後續維護的人很難犯錯, 這也是作者不斷強調的精神, 一連串的討論值得細細思考。

當 enum 的行為只差在數值不同時, 一個 method 只有一種行為, 用一個 method 即可; 但若要提供多種不同行為時, 實作上有許多選擇:

  1. 只用一個 method 使用 switch 來決定行為
  2. 用 abstract method + constant-specific method implementations 各別實作對應的行為
  3. 類似上面的作法, 但將運算轉包給 nested enum, 使用 composition 的技巧 (見後文說明)。作者稱這個為「the strategy enum pattern」

第一個作法最簡單, 缺點是增加新的常數時, 不小心漏寫對應的程式時, 不會 compile error, 也不會有 runtime error。第二個作法透過 abstract method, 漏寫會導致 compilation error。

但第二個作法也有個小問題: 不同方法之間無法重用類似的程式, 而重覆的程式容易造成 bug。於是有第三個作法來避免這個問題, 不過也提昇實作複雜度, 程式變得沒那麼易懂。

書上舉的範例是: 定義 enum PayrollDay 來表示星期幾, 並提供方法依當天的工作時數來計算薪資。書上的範例 code 如下:

enum PayrollDay {
    MONDAY(PayType.WEEKDAY),
    TUESDAY(PayType.WEEKDAY),
    WEDNSDAY(PayType.WEEKDAY),
    THURSDAY(PayType.WEEKDAY),
    FRIDAY(PayType.WEEKDAY),
    SATURDAY(PayType.WEEKEND),
    SUNDAY(PayType.WEEKEND);

    private final PayType payType;
    PayrollDay(PayType payType) { this.payType = payType; }

    double pay(double hoursWorked, double payRate) {
        return this.payType.pay(hoursWorked, payRate);
    }

    private enum PayType {
        WEEKDAY {
            double overtimePay(double hours, double payRate) {
                return hours <= HOURS_PER_SHIFT ? 0 :
                    (hours - HOURS_PER_SHIFT) * payRate / 2;
            }
        },
        WEEKEND {
            double overtimePay(double hours, double payRate) {
                return hours * payRate / 2;
            }
        };
        private static final int HOURS_PER_SHIFT = 8;

        abstract double overtimePay(double hours, double payRate);

        double pay(double hoursWorked, double payRate) {
            double basePay = hoursWorked * payRate;
            return basePay + overtimePay(hoursWorked, payRate);
        }
    }
}

這個寫法有兩個特色:

  • 將實際的計算外包給 PayType, 藉此共用算法
  • 透過 constructor 強迫選擇 PayType。若有新的情況, 比方新年假期要算兩倍薪資, 開發者會記得寫新的 PayType。

依作者的思維, 讓維護的人不易犯錯比較重要。在這前提下, 讓程式沒那麼直接, 是可以容忍的 trade-off。我個人也認為如此, 開發者的功力會持續進步, 就愈來愈能看懂這類「進階技巧」, 而不會覺得這類寫法比較難懂。長遠來看, 是比較好的選擇。

2011年7月1日 星期五

Effective Java 讀書筆記: Item 10 - 總是要覆寫 toString

記錄讀書心得, 內容不一定和書上一致, 有些是我自己的看法。

toString 方便顯示訊息給使用者看和除錯, 要實作它應該沒什麼爭議。作者一些額外的考量:
  • 是否要在文件 (註解) 裡說明詳細的格式, 並傳回包含所有資訊的字串。附帶好處是可用來和物件轉換, 方便和外界輸入輸出, 或寫入硬碟作為永久資料。可以是XML、JSON 或自己訂的特殊格式
  • 提供一個 static factory method 轉換字串會更方便
  • 缺點是, 一但明確在文件中訂了格式, 日後就不方便更改。感覺得出來作者一直都很強調「contract 」, 非常小心看待 public API 之類的事
  • 字串包含的任何資訊都要有對應的取得方法, 不然會讓程式設計師 parse toString() 的結果, 成為易錯且難以擺脫的 de factor API
btw, 我發覺寫讀書心得的難處之一是, 得想辦法將英文的概念轉成中文, 有些說法在英文裡很直覺, 但直譯成中文會很怪, 也可能是我不習慣這些詞的中文用法吧。

Effective Java 讀書筆記: Item 9 - 覆寫 equals 時, 一定要覆寫 hashCode

記錄讀書心得, 內容不一定和書上一致, 有些是我自己的看法。

覆寫 equals 卻沒覆寫 hashCode 的話, 使用 HashSet / HashMap / Hashtable 或任何用到 hashCode 的 class 就會出包: 使得兩個邏輯上相等的物件, 在 hash 的階段就被視作不同物件, 而無法找回來。

實作一個好的 hashCode 是一門學問, 作者列了很多注意事項, 看描述不如看 code, 引用 String 的 hashCode:
@Override
public int hashCode() {
    int h = hash;
    if (h == 0) {
        int off = offset;
        char val[] = value;
        int len = count;

        for (int i = 0; i < len; i++) {
            h = 31*h + val[off++];
        }
        hash = h;
    }
    return h;
}
value、offset、count 是 member field。不同 String 之間有共享 value, 用來避免在呼叫 substring 後增加重覆內容, 因此浪費記憶體。value[offset] ... value[offset + count -1] 是這個 String 實際用到的內容, 所以計算 hash code 時只看這些字元。

上面的程式有幾個重點
  • result = 31 * result + c 的數學形式
  • 選用質數當乘數較好 (如31), 別選 2 的倍數, 乘到溢位就都變零了
  • 31 的另一好處是近代 JVM 會做最佳化, 31*h 會自動轉成 (h<<5) - h
  • 用乘法可確保相同字元在不同順序的情況下, 會有不同的 hash code
其它相關要點
  • 各種 primitive type 都有轉為 int 的方式, 像 long n 可用 (int)(n ^ (n>>>32))、float f 可用 Float.floatToIntBits(f)
  • 寫 unit test 確保邏輯上相同的物件有相同的 hash code
  • hashCode 計算量太大的話, 可考慮用 lazy evaluation + cache
  • 別為了省計算時間而簡化產生 hash code 的方法, 這會反映在 hash table 的 collision 上, 資料量大時可能更不划算

在 Fedora 下裝 id-utils

Fedora 似乎因為執行檔撞名,而沒有提供 id-utils 的套件 ,但這是使用 gj 的必要套件,只好自己編。從官網抓好 tarball ,解開來編譯 (./configure && make)就是了。 但編譯後會遇到錯誤: ./stdio.h:10...