2014/1/20

Why erlang? What can erlang do for us?

Erlang 是一種通用的平行導向程式語言,老爸是Joe Armstrong,想開發出一個共時、分散式、多核心、容錯的系統,Erlang 是個絕佳的選擇。

C10K Problem

著名的 The C10K problem 告訴我們,在高速網路與便宜的硬體的帶動下,如果一台機器能服務更多使用者,就代表著每一個客戶端消耗掉的運算資源更少,成本更低。

要解決這個問題,最重要的是要處理網路IO與硬碟IO,這些方法的區隔與不同之處如下:

  • 決定要不要在 single thread 中處理多個 I/O calls,要怎麼做?

    1. 不使用single thread處理多個 I/O 的方法,還是沿用 blocking/synchronous calls 並持續以 multiple threads or processes 來處理 concurrency 的問題

    2. 使用 nonblocking calls 啟動 I/O (e.g. write() on a socket set to O_NONBLOCK),並以有資料需要讀取的通知 readiness notification 來告訴該 channel 要啟動一輪新的 I/O (e.g. poll() or /dev/poll) 這種方法只適合 network I/O 不適合 disk I/O

    3. 使用 asynchronous calls 啟動 I/O (e.g. aio_write()),處理完成後發出通知(例如 signals or completion ports),這種方法同時適合 network 與 disk I/O。

  • 如何撰寫處理每個客戶端的程式碼?

    1. 每一個 client 使用一個 process (這是Unix 自 1980年開始,就一直採用的方法)

    2. 一個 OS-level thread 服務多個 clients,每一個 client 透過以下某個方式控制

      2.1 一個 user-level thread (例如 GNU state threads, classic Java with green threads)

      2.2 a state machine (a bit esoteric, but popular in some circles; my favorite)

      2.3 a continuation (a bit esoteric, but popular in some circles)

    3. 一個客戶端使用一個 OS-level thread (e.g. classic Java with native threads)

    4. 一個 active client 使用一個 OS-level thread (e.g. Tomcat with apache front end; NT completion ports; thread pools)

  • 不使用標準 OS services,直接將程式碼放進 kernel (e.g. in a custom driver, kernel module, or VxD)

下面的五種是最常見的組合方式

  1. 一個 thread 服務多個客戶端,使用 nonblocking I/O 和 level-triggered readiness notification
  2. 一個 thread 服務多個客戶端,使用 nonblocking I/O 和 readiness change notification
  3. 一個 thread 服務多個客戶端,使用 asynchronous I/O
  4. 一個 thread 服務一個客戶端,使用 blocking I/O
  5. 把程式碼編譯進 kernel

Currency in Erlang

共時導向編程 提到「共時」有兩種不同的作法,分別是「共享狀態共時」,與「訊息傳遞共時」。大多數的主流語言(Java、C#、C++)採用「共享狀態共時」的設計,但 Erlang 採用「訊息傳遞共時」。

在Erlang中沒有共享狀態,唯一一種交換資料的方式,是透過「非同步」訊息傳遞。Erlang的共時單位稱為行程(process),但其實是類似執行緒的地位,而不是真正的OS process。Erlang的行程之間彼此透過訊息傳遞方式來進行溝通,每個行程都可以在不同的核心,不同的CPU,甚至不同的電腦上執行。

對照到上面的 C10K 的解決方案,Erlang 的方法是一個 thread 服務一個客戶端,使用 asynchronous calls 啟動 I/O,處理完成後發出通知,從上面的分析,我們也知道這種方式,同時可以處理 網路 與 Disk I/O。

Java設計分佈式網絡架構的不足及與Erlang的比較 中提到,傳統的 RPC (Java RMI) 都是錯誤的,我們不應該想辦法把遠端呼叫包裝成跟 local call 一樣。Erlang 沒有 RPC,只有 message send/receive,receiver也能設定 timeout。

Dig into Erlang

雖然 Erlang 天生的分散式及語言本身的特性,讓他非常適合實作一個多工、容錯、分散式的系統,但並不是學會了 Erlang 的語法,了解了 OTP,就代表我們可以完美地解決 C10K Problem,甚至能處理到 C500K Problem,這牽涉到語言熟悉度,以及一些性能調整的技巧,換句話說,我們需要花時間深入 dig into the world of Erlang。

從 C10K 到 C500K 這篇文章說,現在大家開始在談 C500K,而不是 C10K 了,也提供了資訊(Linux Kernel Tuning for C500k),使用 Java+NIO 的方式,達成 C500K 的指標。

其實要達成 C500K,也不一定要用 Erlang,用 Java+NIO 也可以。那麼是不是就不要用 Erlang 呢?Erlang 還有其他優勢是 Java 欠缺的,那就是容錯與分散式。

當我們以 Erlang OTP 撰寫服務時,可以很快速,在不需要修改太多程式碼的狀況下,將程式放在多個 Erlang VM 中運作,而且這幾個 Erlang VM 可以在單一或多個實體機器上執行,Erlang OTP 也內建了monitor機制,可以設定 process 存在的條件,在發生錯誤時,自動重新啟動,或做對應的處理。這樣的優勢在 Java 開發上完全是看不見的。

世界是平行的

當我們搜尋 Erlang 時,在繁體中文的網頁中,一定會搜尋到蔡學鏞先生寫的 Erlang:世界是平行的!,還有他翻譯而且已經絕版的 Erlang程式設計 這本書。Erlang的相關資料除了英文,還有對岸 Erlang資源列表 產出了很多翻譯的資料,背後的原因,當然是經濟規模。

在台灣的軟體開發,要求要能處理 C1K 已經算是個大型的系統了,一般中小企業的系統,同時大概都是幾十到幾百個使用者使用而已,在大陸,一個城市的經濟規模的最大值就可能遠遠超過台灣的狀況。

但我們也不需要妄自菲薄,大陸的軟體公司,也不是每一個都有需要作到 C10K,也有很多中小企業需要的軟體,並不是每一個系統,都要杞人憂天,幻想會有超多的使用者同時連線進來使用。畢竟系統都是在一個進化的過程中,慢慢地因應需求,逐漸調整到最佳狀態。

erlang 的語言特色

  1. 平行導向程式設計: 以 spawn/* 函式,將函式設定為獨立的 process,後續使用訊息互相傳遞,進行跨行程通訊
  2. functional programming
  3. single assignment: 變數只能被設定一次,不能異動
  4. dynamic data type: 動態資料型別
  5. exception catching: 以 try catch 進行異常處理
  6. code hot deployment: 在不中斷系統的情況下,進行程式替換

LYME/LYCE

在 open source 軟體堆疊中,如果要建構一個支援動態網頁內容的網站,相對於常見的 LAMP:Linux, Apache HTTP Server, MySQL, PHP,在 Erlang 環境有著 LYME/LYCE:Linux, Yaws, Mnesia/CouchDB, Erlang 的軟體bundle。

要開發網頁程式,LYME 也能提供完整的開發環境,而且除了 Linux 沒什麼好挑的之外(其實可以選擇自己慣用的 distribution,我們是用 CentOS),網頁 Server Yaws也有替代選擇:Mochiweb、Misultin、Cowboy,資料庫 Mnesia 的替代選擇為:CouchDB、Riak、SimpleDB、Cloudant、Couchbase Server。

Prepare to Jump Higher

C1K 已經很夠用了,那我們選擇學習 Erlang 的用意在哪?就是為了要「準備」,公司目前雖然還是在小企業的規模中掙扎,一開始也不自我膨脹,引入外部資金來支撐一些線上服務的夢想。

我們是雙面人,一方面要激進地嘗試不同的領域與技術,另一方面又是保守的,就算是要燒錢,也要在更有把握的條件下孤注一擲。

接下來我們會嘗試從基本語法開始,認識 Erlang 的語言特性,然後是 OTP,最終目的是產出一個 open source 專案。

Erlang 的爸爸:Joe Armstrong

最後要認識一下 Erlang 的爸爸 Joe Armstrong,SICS首頁 有他的照片。1986年釋出了第一版的 Erlang,到今天已經過了28年。

Github:http://joearms.github.io/
SICS:http://www.sics.se/~joe/
Blog:http://armstrongonsoftware.blogspot.tw/ 已經很久沒有更新了)

2014/1/18

一位 pro leader 該有的條件

先前的文章我眼中的 pro programmer,討論到一位專業的程式設計師,該有什麼樣子,當時的文章最末段也想過,該找個人寫篇 pro leader 的模樣典範,而今天剛好看到InfoQ提供了一 篇文章的翻譯:優秀的領導與差勁的領導,原文是 Vlad Mihalcea 的一篇文章:Good vs Bad Leader,內容是討論一位專業的 leader 該是什麼樣子。

翻譯文章的內容雖然跟原文一樣,但是卻放棄了遵循原文表格對照的寫法,這降低了文章的可讀性。我並沒有得到原作者的同意,可以將整篇文章逐字翻譯,但我想我可以原作者的表格方式,然後嘗試用簡單幾句話做個整理。

context good leader bad leader
負責 承擔自己專案的成敗 永遠會怪別人
努力工作 以身作則,不管有趣或無聊的工作都做 不再寫程式
指導新人 注意新人的狀態,懂得在放手跟要求中間拿捏分寸 新人就是要操他才會進步
尊重 尊重每一個人 成員有人犯錯就落井下石
升遷 相信技術與專業會得到回報 沒料,只會逢迎拍馬、耍嘴皮子
信任 相信團隊成員,大家在互動中成長 不相信別人,技術差的只能做寫文件或做單元測試
任務分配 做大家不願意做的事 只會選自己喜歡的事做,困難的推給其他成員解決
問題回報 盡可能解決問題,也會及時向上回報狀態 掩飾問題,紙包不住火時就找替死鬼
代碼審查 相信 code review 的價值,重複發生的問題就寫到共享 blog 中 不做 code review,大家各自為政
挫折 吸收別人的經驗,提醒自己不重蹈覆轍 讓成員也經歷自己經歷的錯誤
新知 重視傾聽,接受 brain storming 對無聊的建言嗤之以鼻,偷取成員提出有趣的想法,往上回報。

關於挫折的部份,原作者覺得 bad leader 會讓成員也經歷自己經歷的錯誤,雖然 leader 該想辦法讓成員避開一些顯而易見的石頭,應該給新人一些必要的協助,但我認為人類有時候,就是會有「不經一事,不長一智」的狀況,沒有痛過就不懂得要如何成長,總有些時候,是其他人不管怎麼苦口婆心嘮嘮叨叨,可是當事者就是沒辦法體會別人好意的提醒的時候,這也只能讓事情發生,然後再回過頭來一起檢討。

原文把團隊成員都假設是個成熟的個體,能跟 leader 有著良好的互動,但世事並非一直都很如意,有時會遇到一些無法跟團隊融合的個體,發生這種狀況時,已經沒辦法討論誰對誰錯了,一個 leader 就該承擔起來,採取適當的行動,如果真的沒辦法解決,也只能壯士斷腕,避免問題擴散到其他成員身上,畢竟一個 team 一定會有個共用的習慣與潛規則,這是不容被質疑與打破的默契。

我認為自己針對團隊管理上需要下些決定時,心裡抱持著的最高指導原則是:「我希望大家都能發揮自己的優勢,逐日進步,以接受更多的挑戰。」挑戰不單指是更高超的技術,有時候是為了大家可能會遇到的下一個職業生涯階段需要做的準備與練習。這些動作會讓直接減輕我在一些專案上的 coding effort或是管理的 effort,感覺上像是我的工作內容減少了很多,其實我是把現在手上的工作交給適當的成員,然後設法包些其他的事情去做,有點像是一直擴大工作半徑的團隊同心圓的感覺。

因為我在專案中不是扮演實作的角色,而是在建立與定義商業邏輯與規格,分配工作,盯進度,確認成品的品質...等等。有幾次我以講笑話的方式脫口而出:「反正我也不用寫這些程式」,但其實講出來之後,卻又後悔覺得這樣講並不恰當,我不曉得有沒有人在意,「說者無心、聽者有意」。其實我有時也會想,能夠像以前一樣其實也很好,不講什麼話,專心coding,不用刻意注意每個成員的狀態,又能更深入熟悉基本的技能,不會停留在懂得皮毛的狀態。但我現在已經不能真的這樣做了,這樣反而有可能變成搶了成員表現自己的機會,也可能造成一些不信任感。

team leader 該要注意 member 的狀態,處理跟人有關的問題,也有技術上的基本條件,因為不懂技術,無法從根本上驅動團隊成員,也不知道怎麼拿捏評斷每一個人能力的標準,也無法適當地判斷並分配工作項目,否則也只是會造成團隊產出的成品,砸到自己的腳的狀況。

2014/1/15

我眼中的 pro programmer

一個 pro programmer 工作者,在 team leader 的眼中,存在著某些決定性的條件,而以下是我個人認定,專業 Programmer 所該存在的幾項條件。

smart copy-and-paste coder

大部分的 programmer 都必須上網翻閱問題與資料,有時候可以直接找到其他人做的解決方案,有可能是個簡短的 hack,或是套用某個 library。

如果有製作一個完整功能經驗的programmer,不管是 library或是獨立的 APP,通常會發現,如果開發的時候,手上只有一份規格文件,以這樣的基礎去開發時,會遭遇到許多困難。這時如果已經有人把功能做出來了,直接閱讀他的範例,問題就解決一大半了。

smart copy-and-paste 並不是看到什麼就照抄,除了一些特殊的 hack codes 之外,搜尋到解決方案或 coding 方法之後,必須要先看懂別人寫什麼,然後再「適當」地套用在自己的專案上,不懂 code 的內容,閉著眼睛照抄,可能會帶來一些無法理解的副作用,還會落下一句「我不知道為什麼會這樣!」,不懂得前因後果就照抄是絕對不行的。

bug-free codes producer

基本上如果是 Java 這種 Strong Typed 語言,程式在編譯時,就已經檢查掉不少基本的語法錯誤了,而 bug 的發生,最常見的原因就是 programmer 「偷懶」,例如:method 的參數會發生 NullPointerException,就是 programmer 偷懶沒有寫檢查,因為他自己「假設」這個欄位不會是 null,或是一些參數,送入了預期之外的數值。

多寫幾行程式,就可以降低發生問題的機會,產出品質更高的 codes,如果實作時隨時都想偷懶,自己做了太多「假設」,bug-free codes 永遠都在遙遠的夢想中,尤其是business method 的界面與網頁 API 界面的參數限制。

有個概念是,越多程式碼,發生問題的機會越高,這也沒錯,但那是指業務邏輯的部份,在做程式分析時,必須想辦法釐清業務邏輯,以最精簡的判斷方式撰寫,一堆繞來繞去又重複檢查的程式邏輯,既會讓人讀不懂,又會增加發生錯誤的機會。

defect digger

每個系統都有自己的歷史包袱,有很多商業邏輯跟假設隱藏在裡面,資料庫的資料,也會有很多邏輯上的相關之處,例如,當 programmer 被分配到實作「為系統實作多國語言」這個功能時,可能只會想到,要修改界面上顯示的文字,但可能還有錯誤訊息,錯誤頁面要處理,錯誤訊息有可能是寫在 Server 直接傳送給 Client,或是系統發送給使用者的 email或通知,這些全部都得考慮多國語言。

重要的是,programmer必須要想到,因為這個功能,有可能影響到的所有其他的功能,在實作該功能的時候,要同時去檢查,有沒有影響到這些功能,或是反過來詢問 leader,這可能是一開始沒有考慮到的相關問題,該怎麼處理,把需求處理做得更完整。

考慮越周全,自然就能挖到更多系統的 defects。 如果 prgrammer 想說,這是 leader 下達的指令不夠周全,我只要照著要求的內容,去改寫程式就好了。這不是專業 programmer 該有的想法,因為 programmer 會是直接接觸程式碼的人,他可以從程式碼,去反向找到系統中所有相關的功能,熟悉系統商業邏輯的人,雖也可以做到類似的事情,但總是間接想到,也只能說「有可能」會影響到某某功能。

problem resolver/terminator

程式的問題可以分成兩種,一種是 bug,程式跑一跑,出現了奇怪的結果,一種是新功能,沒有人做過,不知道怎麼寫。

當程式遇到bug時,第一時間要做的,是懷疑自己是不是有考慮不周全的地方,也要思考,是不是自己的想法與寫法,或是程式的邏輯順序有問題,以此為前提,再往下深入去尋找,是不是有別的方法,可以解決這個問題。

要能被交代處理新功能,某方面就代表說,這個 porgrammer 是受信任,可以找到解決新問題的方法。一個能解決問題的人,總比一直製造問題、產生bug的人來得討人喜歡,且能受重用。

good communicator

當遇到問題你不知道怎麼處理時,除了問google之外,最快的方式是先問問同事,可能已經有人處理過類似的狀況。問問題,不是把問題丟出去就好了,你必須要讓其他人感受到你的責任感,問的時候,要加上自己的觀察,聽的時候,要思考並從回答中,找出模糊地帶,再繼續追問下去。

定期會議或是每天的站立會議,必須要把自己的狀態、進度,有沒有遇到特別的狀況卡住很久了,需不需要幫忙,這些重點事項言簡意賅地說明清楚。

如果有機會面對了客戶,為求專案順利在跟客戶打交道拉關係的前提下,必須跟客戶保持適當的距離,客戶的需求一定要經過 PM 才能接受。

心裡有什麼疙瘩,或是覺得團隊有什麼狀況,要直接跟 leader 或其他同事溝通,悶在那邊是無濟於事的。

aggressive learner, conservative implementater

積極地學習新的技術並了解新的工具與概念,但在團隊合作上,卻要採取比較保守的方式,而不是學了一樣新東西,就想在專案上實驗並套用上去。

新技術相對就帶來了新的風險,當專案時程游刃有餘,或是在前期的研究階段,新技術的測試與實驗是必要且有效的,但在處理舊專案時,必須適當地遷就舊有的程式碼,因為這些程式都是經過多次測試驗證的結果,沒有相當大的好處,或是有絕對的理由,不能輕易放棄既有的東西,但相對地,一旦確認有足夠要放棄的理由,也要義無反顧,適當的提出建言與舉證,強烈地建議要除舊佈新。

good job handler

明確掌握自己的工作進度與心理的狀態,如果有卡住一段時間的問題,主動去問問同事或 leader,自己悶著頭去做,只會讓團隊其他人搞不清楚你在做什麼。做的事情、研究的東西都必須讓團隊確實掌握。

除了自己的狀態,還要注意其他人的進度與狀態,整體的進度必須跟著團隊前進,如果自己落後了,要及時反應狀況,由leader決定需不需要其他人的支援,當然進度超前時,也要及時回報,由leader分配其他的工作。

industrious but agile documentator

在實作某些功能前,前期的業務分析結果,還有一些開發期間用來測試與驗證的 sql & test scripts,必須要確實地紀錄在專案的文件區裡面,而程式碼中,撰寫註解要勤快,撰寫文件與註解並不是無限上綱,規定每一個method、每一個參數、每一個邏輯判斷都要寫上註解,而是要在適當的時機去做這件事,當發覺自己花了一段時間,才釐清該怎麼撰寫時,就是個明確要寫下文件的時機點,寫文件,沒有標準的格式,要以自己認為最適當的方法記錄下來。

rush but careful stepper

積極且迅速的工作效率,但同時也要步步為營,把每一件相關的工作都做好。

如果 leader 每一次來問,都回應說做好了,但是「做好」多少的程度雙方的認知會有落差,通常 leader 認定的做好了,是指都全部測試過,都正常了,而 programmer 認定的做好了,是我把這個 task 完成了,這個 task 也測試過了,但改完這個會不會影響到其他地方,這就跟我無關了。

leader 說的都是針對整個系統,programmer 說的常都是針對某一個工作,因此當 programmer 回應給 leader 時,要清楚地告知有沒有測試,測試了多少項目等等附帶的資訊與條件,leader也會根據這些資訊,得到下一個階段工作的重點項目,或是決定某一個大範圍重新測試的時間點。

結語

當個 team member 跟 leader 的心情與想法是截然不同的,「換個位置就會換個腦袋」,這種情況是無可厚非的,只能說,不管自己在什麼位置,要時時提醒自己,換個角度去看這些事情,自然就可以轉換自己的心情與想法。

其實 team leader 掌握團隊的時候,同時也必須扮演 boss 的手下的角色,也需要一些角色轉換的技巧,才有辦法在這兩者之間,動態自由地轉換。

下次或許該找個人,談一談在他眼中一位 pro team leader 是什麼樣子。

2014/1/10

Groovy in JVM

Groovy 是必須要依附在 JVM 的 script 語言,他的語法是簡化自 Java 的結果,由於她專為 Java 而生的特性,我們只需要附加幾個 groovy 的 jar 檔,就可以使用 groovy。事實上,我們只需要 groovy 當作是一種 XML,XML 有自己的語法規範,在 JVM 會有 XML Parser 協助轉換 XML 為 Java 的物件,而 groovy 就像是一種進階的 XML 文件檔案,他不單能描述資料,也能跟 Java 物件互動,這也是 groovy DSL 帶來的優勢。

以往我們以 XML 作為延伸 Java 系統的第一選擇,但在這些高階 DSL 的協助下,我們可以用 groovy 更精簡的語法,而得到比 xml 更多的功能。

Installation

安裝有兩個部份,第一個部份是標準元件,首先到 groovy 官方網站下載 套件,目前是 2.2.1 版 groovy-binary-2.2.1.zip,通常處理 Java 的套件,我不會選擇使用 windows installer,而是下載 binary package,然後再自行到環境變數設定 GROOVY_HOME 為 c:\java\groovy-2.2.1(我習慣把套件Java都放在 c:\java 裡面),然後再把 %GROOVY_HOME%\bin 放到 PATH 裡面。Linux 環境也是類似的作法,差別只是 Linux 環境的 script 不一樣,通常可以修改 /etc/profile,然後增加 export GROOVY_HOME=/usr/share/groovy-2.2.1 ,增加 export PATH=$GROOVY_HOME/bin:$PATH 。

第二個部份,是安裝 IDE 的 Plugin ,在 Groovy-Eclipse 的 網頁 裡面,選擇適合自己 Eclipse 版本的 Plugin,我是選擇 4.3(Kepler) ,在 Eclipse -> Help -> Install New Software 裡面把 groovy-eclipse 的 http://dist.springsource.org/release/GRECLIPSE/e4.3/ 路徑加進去,就可以安裝Groovy-Eclipse了。

Groovy with Eclipse - Tutorial 給了一篇 tutorial,我覺得作者每一小段都提供簡單的 sample 的方法還不錯,以下我們用類似的方式測試,但我修改了一些 topic 的先後順序。

source code

如果需要以下內容的範例程式,請點擊 連結 下載檔案。

HelloWorld

這是一個列印HelloWorld的範例,前面是用 groovy 做的,檔名為 TestGroovy.groovy,後面是 TestJava.java,兩個範例的功能一樣,但精簡的 groovy 帶來了有效率的開發。

package test.groovy

class TestGroovy {

    static main(args) {
        def mylist=[
            "字串  Hello",
            " World",
            "...",
            ", 數字:",
            1
        ]
        mylist.each{ print it }
        println ""
    }
}
package test.groovy;

import java.util.ArrayList;
import java.util.List;

public class TestJava {
    public static void main(String[] args) {
        List<String> mylist =new ArrayList<String>();
        mylist.add("字串  Hello");
        mylist.add(" World");
        mylist.add("...");
        mylist.add(", 數字:");
        mylist.add(""+1);

        for(String a: mylist) {
            System.out.print(a);
        }
        System.out.println("");
    }
}

跟Java的差別

在Difference with Java 裡面提到了跟 Java 程式碼的差異

  1. Floating point number literals 預設的型別為 BigDecimals
  2. 預設就會 import java.io., java.lang., java.math.BigDecimal, java.math.BigInteger, java.net., java.util., groovy.lang., groovy.util.
  3. == 就等同於 Java 裡的 equals,如果要判斷是不是同一個instance,就要寫成 foo.is(bar)
  4. in 是 keyword
  5. 宣告 array 要寫成 int[] a = [1,2,3] ,而不是 int[] a = {1,2,3};
  6. loop迴圈要寫成 for (i in 0..len-1) {...} 或是 for (i in 0..<len) {...} 或是 len.times {...} ,而不是 for (int i=0; i < len; i++) {...}
  7. Semicolons 句末的分號; are optional
  8. return keyword is optional
  9. static method 裡面可使用 this
  10. Methods and classes 預設為 public
  11. method 拼錯了,只會在 runtime 產生 MissingMethodException,而不會在 compile time 發生,這要參考 Runtime vs Compile time, Static vs Dynamic 這篇文章。在 groovy 2.0.0 之後,支援了 @groovy.transform.TypeChecked 這個 annotation,可在 compile time 提供錯誤檢查。

precompile or direct mode

執行 TestGroovy.groovy 有兩種方式,Groovy簡介、安裝SDK及基本開發環境 有一張說明圖寫得很清楚。



在 Eclipse 的 Run As 裡面執行 TestGroovy.groovy 也會發現有兩種選擇,一種是 Groovy Console,一種是 Groovy Script,選第一種時,會出現一個獨立的 Groovy Console IDE 視窗,可直接在視窗裡面修改 groovy code,然後直接執行,這也就是 direct mode,而選擇 Groovy Script 則是在 Eclipse Console 裡面直接得到執行結果,查看一下會發現,TestGroovy.groovy 會先被編譯成 TestGroovy.class 與 TestGroovy$_main_closure1.class,並放在 bin 目錄裡面,Eclipse console 是使用 JVM 執行這些 Java class 的結果。

在一開始獨立安裝的 groovy-binary-2.2.1.zip 裡面,我們可以在 bin 目錄找到幾個 batch 檔,groovyConsole.bat 就是剛剛 Groovy Console IDE 視窗,文件在 Groovy Console 。 groovyc.bat 可以把 TestGroovy.groovy 編譯成 java class 檔,groovy.bat 則
可以執行 groovy script,但這個並不是執行 groovyc 編譯的 class 檔,在Using Groovy from the command line 得到相關的文件說明。

>groovy -c UTF-8 test\groovy\TestGroovy
字串  Hellolo World..., 數字:r:1

Groovy Classes, Objects and Methods

Groovy class 預設為 public,所有的 groovy source file 都是以 .groovy 作為副檔名。

// 檔案目錄 test\groovy\SumIt.groovy

package test.groovy

class SumIt {

    static sum(a,b) {
        a+b
    }
    static main(args) {
        println sum(1,5)
        println sum(2,4)
    }
}

groovy 的欄位預設為 private,且會自動產生 getter與 setter methods,所以可以直接使用 setFirstName,也可以用 p.lastName 的方式,存取欄位資料。

當我們用 new Person(firstName: "名字", lastName:"姓") 這個方式產生 Person 時,groovy將會自動產生含有這兩個欄位作為參數的 constructor。

使用 == 就等同於在 Java 的 equals,在 Person 類別必須要覆寫 hashCode 與 equals 才有作用。

// test\groovy\Person.groovy

package test.groovy

public class Person{
    String firstName
    String lastName
    int age
    def address

    static void main(def args) {
        Person p = new Person()
        // use the generated access methods
        p.setFirstName("Name")
        // This will still use the generated access method, it is not a direct access!
        p.lastName = "LastName"
        p.age=10;
        p.address = ("Homestreet 3");
        println(p.firstName + " " + p.lastName);

        // use the generated constructor
        Person p2 = new Person(firstName: "名字", lastName:"姓");
        println(p2.firstName + " " + p2.lastName);

        println ""
        println "p==p2 : "+(p==p2);
        println "p.is(p2) : "+p.is(p2);
        p2=p;
        println ""
        println("after p2=p")
        println "p==p2 : "+(p==p2);
        println "p.is(p2) : "+p.is(p2);

        Person p3 = new Person();
        p3.setFirstName("Name");
        p3.age=10;
        p3.lastName = "LastName";
        p3.address = ("Homestreet 3");

        println ""
        println "p==p3 : "+(p==p3);
        println "p.is(p3) : "+(p.is(p3));
    }

    boolean equals(o) {
        if (this.is(o)) return true;

        if (!o || getClass() != o.class) return false;

        Person that = (Person) o;

        if (firstName? !firstName.equals(that.firstName) : that.firstName!= null) return false;
        if (lastName? !lastName.equals(that.lastName) : that.lastName!= null) return false;
        if (age? !age.equals(that.age) : that.age!= null) return false;
        if (address? !address.equals(that.address) : that.address!= null) return false;

        return true;
    }

    int hashCode() {
        int result = (firstName ? firstName.hashCode() : 0);
        result = 31 * result + (lastName ? lastName.hashCode() : 0);
        result = 31 * result + (age ? age.hashCode() : 0);
        return result = 31 * result + (address ? address.hashCode() : 0);
    }
}

執行的結果為

Name LastName
名字 姓

p==p2 : false
p.is(p2) : false

after p2=p
p==p2 : true
p.is(p2) : true

p==p3 : true
p.is(p3) : false

Groovy 允許使用不固定個數的參數,但該optional 參數,必須要指定預設值 c=10。

// test\groovy\SumIt.groovy
package test.groovy

class SumIt {

    static sum(a,b, c=10) {
        a+b+c
    }
    static main(args) {
        println sum(1,5)
        println sum(2,4)

        println sum(2,4,5)
    }
}

Loops

迴圈可使用 list.each 的方式,搭配指定變數 firstName-> println firstName 或是使用內建的 it 變數 println it。

針對 number 變數,有 upto(), downto(), times(),也可以使用 (1..6) 這樣的 range data type,來處理迴圈。

package test.groovy

class PrintLoop {

    public static void main(def args){
        def list = [
            "John",
            "Mary",
            "James"
        ]

        // using a variable assignment
        list.each{firstName-> println firstName }
        // using the it variable
        list.each{println it}

        println ""
        5.times {println "Times + $it "}
        1.upto(3) {println "Up + $it "}
        4.downto(1) {print "Down + $it "}

        def sum = 0
        1.upto(100) {sum += 1}
        print sum

        (1..6).each {print "Range $it"}
    }
}

執行結果為

John
Mary
James
John
Mary
James

Times + 0 
Times + 1 
Times + 2 
Times + 3 
Times + 4 
Up + 1 
Up + 2 
Up + 3 
Down + 4 Down + 3 Down + 2 Down + 1 100Range 1Range 2Range 3Range 4Range 5Range 6

Groovy Data Types

Reference variables

Groovy 的所有變數都是 Reference variables,沒有 primitive variables,即使寫成 int 10,背後還是轉換為物件。

Groovy 允許使用 static and dynamic typed variables,動態資料型別要以 def 作為 data type。

以下的 TypeTest可用來查看 def 型別轉換的狀況。

package test.groovy

class TypesTest {

    static main(args) {
        int i = 1 // Short form for Integer i = new Integer(1)
        int j = i +3
        int k = i.plus(3); // Same as above
        // Make sure this worked
        assert(k==4);
        println i.getClass().getName()
        println j.getClass().getName()
        println k.getClass().getName()

        // Automatic type assignement
        def value = 1.0F
        println value.getClass().getName()
        def value2 = 1;
        println value2.getClass().getName()
        value2 = value2 / 2;
        println value2
        println value2.getClass().getName()
    }
}

執行的結果

java.lang.Integer
java.lang.Integer
java.lang.Integer
java.lang.Float
java.lang.Integer
0.5
java.math.BigDecimal

strings

Groovy 的 string 有兩種,第一種是 " " 包圍的 string,型別為 org.codehaus.groovy.runtime.GStringImpl,這種字串稱為 Groovy String,簡稱 GString,可以直接用 $name 進行替換的運算,第二種是 ' ' 包圍的 string,這就是 Java 的字串,型別為 java.lang.String。

package test.groovy

class StringTest {
    static main(args) {
        def name = "John"
        def s1 = "Hello $name" // $name will be replaced
        def s2 = 'Hello $name' // $name will not be replaced
        println s1
        println s2
        println s1.getClass().getName();
        println s2.getClass().getName();
    }
}

執行結果

Hello John
Hello $name
org.codehaus.groovy.runtime.GStringImpl
java.lang.String

Lists and maps

以 List persons = list[] 定義新的 list,persons.get(i) or persons[i] 可取得第 i 個元素。Map是以 Map[key1:value1, key2:value2,...]的方式定義,可以直接指定某一個位置的內容 map[key]=value。

package test.groovy

class ListMap {
    static main(args) {
        List<Integer> list = [1, 2, 3, 4]
        println list[0]
        println list[1]
        println list[2]

        List<Person> persons = list[]
        Person p = new Person(firstName: "John", lastName:"LastName")
        persons[0] = p
        println persons.size()
        println persons[0].firstName
        println persons.get(0).firstName

        Map map = [:]
        def map2 = ["John":"LastName", "James":"LastName2"]
        println map2["James"]
        map2["Test"] = "Tester"
        println map2["Test"]
    }
}

Ranges

只要Java物件有實作 previous() and next() 這兩個method,且有 implements java.lang.Comparable,都可以用在 Ranges。 0..4 就是 0,1,2,3,4 而 0..<4 就是 0,1,2,3。

package test.groovy

class RangesTest {
    public static void main(args){
        for (i in 0..4) {
            println ("Hello $i")
        }

        for (i in 0..<4) {
            println ("2nd Hello $i")
        }
    }
}

Regular Expression

運算子 說明
=~ Find: 如果pattern在字串中有出現
==~ Match: Pattern必須完整符合整個字串
~String 將String轉換為 regular expression
package test.groovy

class RegularExpressionTest {
    public static void main(String[] args) {
        // Defines a string with special signs
        def text = "James happily ever after"

        // Every word must be followed by a nonword character
        // Match
        if (text==~/(\w*\W+)*/){
            println "Match was successful"
        } else {
            println "Match was not successful"
        }
        // Every word must be followed by a nonword character
        // Find
        if (text=~/(\w*\W+)*/){
            println "Find was successful"
        } else {
            println "Find was not successful"
        }

        if (text==~/^J.*/){
            println "There was a match"
        } else {
            println "No match found"
        }
        def newText = text.replaceAll(/\w+/, "hubba")
        println newText
    }
}

Closures

closure 是可使用在 class 或 method 外面的一段程式碼,語法為{para1, para2 -> code of the closure},para1與para2為後面程式碼的參數。list.each裡面可以使用closure。

package test.groovy

class ClosureTest {
    public static void main(args){
        List<Integer> list = [5, 6, 7, 8]
        list.each({line -> println line})
        list.each({println it})
    }
}

Meta Object Protocol

meta object protocol 可動態在runtime增加 methods與 properties。如果程式中,呼叫了沒有定義的 method/property,就會自動呼叫下列這些 method。

  1. def methodMissing (String name, args) - Called for missing method
  2. void setProperty (String property, Object o) - called for non existing setter of a property
  3. Object getProperty (String property) - called for non existing getter of a property
package test.groovy

class MetaObject {
    def map

    Object getProperty (String property){
        println "Setting this propery"
        return 5;
    }

    void setProperty (String property, Object o){
        println "Hallo"
    }

    def methodMissing (String name, args){
        def s = name.toUpperCase();
        if (s.startsWith("HELLO")) {
            println "This method stats with Hello. Full name $name"
        } else {
            println "This method is missing"
        }
    }

    public static void main (args){
        def test = new MetaObject();
        test.hall();
        test.helloMethod();
        test.Hallo();
        test.test= 5;
        println test.test;
    }
}

Operator overloading

groovy支援在class裡面覆寫一些標準的 operations,例如 a+b 可以透過實作 plus method 調整 a+b 的作用。

Operator 名稱 Method
a+b plus a.plus(b)
a-b minus a.minus(b)
a*b star a.multiply(b)
a/b divide a.div(b)
a%b modulo a.mod(b)
a--, --a decrement a.previous()
a++, ++a increment a.next()
a**b power a.power(b)
a-b minus a.minus(b)
a-b minus a.minus(b)

File IO

將 userlist.txt 檔案內容,加上行號,填寫到 userlist2.txt 裡面

package test.groovy

class FileTest {
    static main(def args){
        File file = new File("d:/temp/userlist.txt")
        File file2 = new File("d:/temp/userlist2.txt")
        if( !file2.exists() ) {
            file2.createNewFile() //if it doesn't already exist
        }
        assert file2.exists()
        def file2writer = file2.newWriter()
        file2writer<<"";

        def lineNumber = 0;

        file.eachLine{ line ->
            lineNumber++
            file2 << "$lineNumber: $line\n"
        }
    }
}

XML

使用 XmlParser 處理 XML 文件

package test.groovy

class XMLTest {
    static void main(args){
        def xmldocument = '''
    <persons> 
      <person><firstname age="33">John</firstname><lastname>Chen</lastname></person>
      <person><firstname age="34">James</firstname><lastname>Lee</lastname></person>
    </persons>
    '''
        def persons = new XmlParser().parseText(xmldocument);
        def allRecords = persons.person.size()
        println("Number of person is: $allRecords")
        def person = persons.person[0]
        // name is the name of the XML tag
        println("Name of the person tag is:" + person.name())
        // text gets the text of the node firstname
        println(person.firstname.text())

        // Lets print out all important information
        for (p in persons.person){
            println "${p.firstname.text()} ${p.lastname.text()}"
        }
    }
}

Thread and Concurrency

groovy 跟 java 一樣,可以使用 thread 跟 synchronzied block。

package test.groovy
import java.util.concurrent.atomic.AtomicInteger

class SynchronizedTest {
    private final myLock = new Object()

    static synchronized void greet() {
        println "world"
    }

    synchronized int answerToEverything() {
        return 42
    }

    void foo() {
        synchronized ("myLock") {
            println "bar"
        }
    }

    private def counter = new AtomicInteger()
    synchronized out(message) {
        println(message)
    }

    void processthread() {
        def th = Thread.start {
            for( i in 1..8 ) {
                sleep 30
                out "thread loop $i"
                counter.incrementAndGet()
            }
        }

        for( j in 1..4 ) {
            sleep 50
            out "main loop $j"
            counter.incrementAndGet()
        }
        th.join()

        assert counter.get() == 12
        out "result:"+counter.get();
    }

    static main(args) {
        SynchronizedTest test=new SynchronizedTest();
        test.processthread();
    }
}

另外還有 Groovy GPars 支援 Actors, Map/Reduce, Dataflow, Fork/Join 這些平行處理的功能。

NullPointerException

groovy 可以用 ?: 或是 ?. 來處理 java 最常遇到的 NullPointerException。

package test.groovy

class AvoidNullPointer {
    static void main(args){
        Person user = null;
        def firstName = user?.firstName;
        println "firstName:"+firstName

        user = new Person();
        def firstName2 = user?.firstName;
        println "firstName2:"+firstName2

        def user2
        user2 ?: new Person();
        def firstName3 = user?.firstName;
        println "firstName3:"+firstName3
    }
}

Java class 跟 groovy script 的互動

我們在 Person.groovy 定義的Person物件,可以直接在TestJava.java 裡面直接使用 Person。

// test\groovy\Person.groovy
// test\groovy\TestJava.java

package test.groovy;

public class TestJava {
    public static void main(String[] args) {

        Person p = new Person();
        p.setFirstName("Firstname");
        System.out.println("p="+p.getFirstName());
    }
}

結語

groovy 就等同於一個簡化 Java 語法的內建動態語言,她必須跟 JVM 共生,嵌入 JVM 雖代表她天生就受限於 JVM,但也表示我們可以很容易地以Java方式整合 groovy。

2014/1/5

DSL in Action - written by Debasish Ghosh

看過 Martin Fowler 在 InfoQ 的演講影片 Introduction to Domain Specific Languages之後,接下來,選擇看一本 DSL in Action 的書,這本書的內容涵蓋 JVM 所能支援的 DSL,並從各種角度,去分析實現 DSL 方法的優劣。這本書分兩個部份,第一個部份是 1~3 章,再加上 Appendix A,第二個部份則是 DSL 的實作範例,接下來先看第一個部份。

DSL

DSL 的功能是將 problem domain 對應到 solution domain,首先要找出兩個 domain 之間的共通語彙(vacabulary),透過這個共通的專有名詞,來建立 domain expert 跟 IT system 之間的溝通模型。換句話說,就是要從 domain expert 所描述的業務邏輯中,找到該 domain 的專有名詞、專用術語,再以這個專用術語為基礎,建立溝通互動的語言,形成雙方都能使用的 DSL。

Programmer 設計 DSL 時,要注意幾個地方:

  1. 要一直把使用者放在心上,因為 DSL 的好壞,不好用的話,會直接影響到使用的意願,那就失去了設計 DSL 的原意,沒有人用的 DSL 就是個失敗的設計。

  2. DSL 只需要針對該業務範圍進行抽象化的設計,沒有多餘的東西,太複雜冗長的語言,只會講低使用者的使用意願。

DSL 分為 Inernal、External、非文本DSL 三種。

DSL 的優缺點

DSL 的優點很容易理解,因為適度的抽象化,可幫助使用者更容易處理複雜的問題,試想,如果沒有 SQL ,那麼我們應該怎麼操作 Relational DB,要直接用 API 下 select() 嗎?

除了優點之外,我們更要注意 DSL 的缺點

  1. 設計DSL是很困難的工作
  2. DSL 需要大量前期設計,投入的人力成本
  3. DSL 增加的中間層,可能會有性能憂慮
  4. DSL 有時缺少足夠的編輯工具
  5. 可能會造成「學不完的DSL」現象
  6. DSL 可能導致語言之間的摩擦:開發APP可能需要同時使用多個 DSL,也因此可能造成整合上的問題

良好的抽象應具有的特質

DSL 牽涉到簡化業務邏輯的設計,這是一種抽象化的過程,我們要知道,要從哪些指標判斷抽象化的優劣

  1. 極簡:只開放使用者需要使用的功能,沒有多餘、外露的內部實作內。例如 API 要回傳 Map 而不是 TreeMap,可使用繼承的方式,隱藏內部的實作設計。
  2. 精煉:抽象化的內容不包含任何非本質的細節,移除不必要的細節,可利用 DI(Dependency Injection)隱藏實現的細節
  3. 擴展性:抽象設計可在不影響現有使用者的情況下,持續改進升級。利用 mixin、functional programming 的 closure、open class 的方式達到擴展性
  4. 組合性:可與其他抽象設計組合成更高階的抽象設計。使用 Command、Decorator Pattern。但可能會在multithread環境下造成問題。

實作 DSL 的方法

第二章一開始,以一個證券交易的實例設計 DSL,第一個版本是用 Java語法,但遇到交易員不熟悉 Java 語法的問題,而且 Java 語言裡有過多跟證券交易無關的東西。第二個版本是 XML,XML適合描述文件結構,不適合拿來作為 DSL,而且XML有太多無關的標記。因此又有了第三個版本,是使用 Groovy,這個版本才勉強有了個適當的 DSL。

Internal DSL 的分類

  1. 生成式:編譯後,轉換生成實作語言的的code

    1.1 編譯時meta programming:Lisp, Template Haskell

    1.2 執行時meta programming:Ruby, Groovy

  2. 內嵌式:領域專用的類別內嵌於宿主語言的類別系統

    2.1 Smart API:Java, Ruby

    2.2 AST:Java, Ruby, Groovy

    2.3 內嵌類別:Haskell, Scala

    2.4 反射式meta programming:Ruby, Groovy

External DSL 的分類

  1. 上下文驅動的字串操作

  2. xml 轉換成可使用的資源

  3. DSL 工作台

  4. DSL 中內嵌異質代碼

  5. 基於解析器組合子的 DSL 設計

實作 DSL 的方法有很多,我們應該如何選擇一個最適當的方法呢?要考慮的因素如下:

  1. 重用現有的機制:利用強大的宿主語言提供的功能,例如 Scala或 Haskell
  2. 充分利用現有的知識:要根據開發團隊現有的知識水準來選擇實作的方法。
  3. 外部DSL的學習曲線:外部DSL可能會很複雜,必須要把學習曲線納入開發成本。
  4. 適當的表現力:Internal DSL有重用宿主語言的優勢,但相對也約束了描述業務領域的表現力
  5. 組合性:DSL跟宿主語言之間的是不是能簡單地整合起來

DSL Driven Application Development

在開發 JVM 環境的 DSL 時,最重要的就是要看怎麼跟 JVM 整合在一起,開發的時候,要注意三個問題:1. 整合問題 2. 異常與錯誤的處理 3. 性能的表現。

如果要在一個系統裡面,同時使用多個 DSL,對於 JVM 來說,整合是不成問題的,而且我們可以選擇使用 Java、Groovy、Spring、JRuby、Scala 這些語言來實作 DSL,直接分別將這些實作包裝成 jar,就可以讓主程式引用了。

Internal DSL: Groovy

如果選擇使用了 Groovy,有兩種方式可以將 Groovy Script 整合起來:

  1. 使用 javax.script 的 Script Engine,Script Engine 是一種 sandbox,Groovy DSL 跟 Java Class 之間無法互通,另外在出現 Exception 的時候,stack trace 顯示的行號沒辦法直接對應到 DSL 中的行號,不容易除錯。所以在整合 Groovy DSL時,要優先考慮使用第二種方法。

  2. 使用 groovy.lang.GroovyClassLoader,以 Groovy 實做的 Order 類別,可直接讓 JVM 使用,在運作 dsl script 之後,也可以直接回傳 Order 的 List,Groovy 跟 Java 之間的互動比 Script Engine 的方法整合地更緊密。

Internal DSL: Spring

Spring 2.0 版之後就支援使用 Ruby、Groovy 實作的 Bean,還能直接運用 Spring 的 DI 功能,動態注入 script code,下面是一個定義 bean 的範例,透過 refresh-check-delay 的設定,spring 將會在每5000毫秒檢查script是否有被更新,而自動 refresh 載入的 bean,系統就可以在不關機的條件下,直接更新業務邏輯。

<lang:jruby
    id="accIntCalcRule"
    refresh-check-delay="5000"
    script-interface="org.spingframework.scripting.AccruedInterestCalculationRule"
    script-source="classpath:RubyAccruedInterestCalculationRule.rb">
</lang:jruby>

External DSL: XML

使用 XML Parser 進行文件的解析與處理。

External DSL: ANTLR、JAVACC

ANTLR(Another Tool for Language Recognition)裡面包括了 詞法分析器(Lexer)、語法分析器(Parser)、樹分析器 (tree parser) 的功能,編寫文法(詞法規則與語法規則)描述文件之後,交給ANTLR,就能生成以 Java 語言實作的 parser 程式碼,ANTLR 3.X支援生成 Java,C#,JavaScript,C 這幾種語言的程式碼。

感想

會去看這本 DSL in Action 根本是個意外,原本是要了解 Gradle,然後知道了 Groovy,漸漸地 dig in,最後到了 DSL。讀了幾篇文章後,才了解到,我們在設計系統時,考慮的系統設定模組、外部 API 模組,其實都屬於一種 DSL,我們也可以在設計系統設定時,採用不同於 XML 的方法,設計 API,也可以考慮使用 Groovy實作。而且看了之後,才知道還有太多專有名詞還不了解,現在頂多只能懂得一些皮毛。

以沒寫過 Groovy, Ruby,更別說會寫 Haskell, Scala的狀況,要能深刻了解這本書的內容,還真的有些吃力。接下來,應該把注意力先放回到Groovy。