Tuesday, September 26, 2017

awk style 的 shell script

有一段時間我很喜歡寫一種特定形式的 shell script ,我自己稱之為 awk style shell script 。它通常是長成這個樣子:

這種風格的程式碼,我之前寫得很爽,因為覺得寫起來很快,字數、行數都少。後來回頭看的時候,雖然覺得有些優點、也有些缺點,卻也不知其所以然,最近終於想出原因了。

這種風格它有幾項特色:
1.  主要使用 awk
2.  awk 指令用 unix pipe 串接

仔細分析這種風格使用的程式語言的概念:
1. 每一行的 awk 指令,可以視為是函數呼叫,而且是「高階函數」(higher order function)  。是高階函數是因為 awk 指令都隱含了一個迴圈、每一行就是對應一次的迴圈操作。
2. 每一行的 awk 指令裡的部分運算內容,可以視為是傳遞進入高階函數裡的運算,換言之,它們可以視為是匿名函數
3. unix pipe 之間,是通過有特定格式「字串流」 (string stream) 來傳遞資料。 其『特定格式』是表格的形式,以斷行字元 (\n) 來分隔 row,以空白字元 (\s) 來分隔 column

有了上述的分析,要講出優點就容易多了:
1. 因為使用了高階函數的概念,要寫的 boilerplate code 就變少了,寫起來自然快。
2. 因為使用了匿名函數,一方面減少了 boilerplate code ,還省去了函數命名的心理負擔。
3. 使用有特定格式的字串流來傳遞資料,因為特定格式通常可以描述輸出所需要的完整資料,程式就可以變成「單向的資料流」。程式要復用的時候,通常就是截取資料流之中,可以復用的部分來復用。
此外,這種使用字串流的風格與 C style 的程式設計最大的差異點在於: 「C style 的程式設計風格是只要是不會被函數修改的資料,都盡量不要放進函數的輸入、輸出。」這是因為執行效能考量,減少不必要的拷貝。付出的代價則是,往往為了輸入輸出引數的一點點變化,要寫許多重覆的程式、或是套接 (adapter) 的程式碼,因而減少了開發效率。

4. Unix pipe 雖然不是惰性求值 (lazy evaluation) ,而是 buffered queue ,但是使用 unix pipe 寫出的 shell script 卻是惰性求值的風格:『不需要管資料的長度』。

另一方面,也有缺點,但是要克服缺點,就需要使用 python/clojure 之類的語言才容易克服了。
1. 每一行 (row) 的字串資料對應的 awk 指令,其實就是 functional programming 裡的 map/filter 操作。改成用 map/filter/reduce 來寫,表現力不會打折扣,可讀性還會提高。
2. 匿名函數如果要復用時,還是給它取個名稱比較好。
3. 使用特定格式的字串流來傳遞資料,不如使用 list 或是 hash map 來傳遞資料。後者的表現能力更強、而且還可以夾帶型別資訊

Thursday, September 21, 2017

快速測試 clojure library

在開發 clojure 的時候,有時候會想要快速地測試一個新的 library ,但是,只是快速地測試,卻得要為這個 library 做 jar 的下載、並且在 project.clj 裡設置路徑,也太麻煩了。有沒有比較簡單的方式呢? 可以用一個 leiningen 的 plugin 來辦到這件事。

<安裝>
修改 (~/.lein/profiles.clj) :
{:user {:plugins [[lein-try "0.4.1"]]}}
<實測>  測試 cheshire 這個 json library
vagrant@owl-docker:~/workspace/src/github.com/clo/test$ lein try cheshire
nREPL server started on port 53192 on host 127.0.0.1 - nrepl://127.0.0.1:53192
REPL-y 0.3.7, nREPL 0.2.12
Clojure 1.8.0
Java HotSpot(TM) 64-Bit Server VM 1.8.0_144-b01
    Docs: (doc function-name-here)
          (find-doc "part-of-name-here")
  Source: (source function-name-here)
 Javadoc: (javadoc java-object-or-class-here)
    Exit: Control+D or (exit) or (quit)
 Results: Stored in vars *1, *2, *3, an exception in *e

user=> (require '[cheshire.core :as json])
nil
user=> (json/generate-string {:foo "bar" :baz 5})
"{\"foo\":\"bar\",\"baz\":5}"
user=>

Datomic 學習/使用心得

之前在研究 object-relation impedance mismatch 的時候,發現了 Datomic 這個資料庫,據說這個資料庫可以有效地迴避這個 object-relation impedance mismatch ,於是我找到機會後就來用看看。

學習的過程大概是這樣子:
1.  下載 datomic-free 的 docker 來用,並且寫 clojure 程式將資料塞入資料庫。
2.  註冊 datomic  的網站,取得 datomic 的 web console
3.  上一個 learn datalog today 的網站練習 Datomic 的 Query 怎麼寫。

踩過的坑:
(1) group-by
本來一度以為,Datomic 沒有支援 group-by 的語法。後來仔細查才發現,原來是 implicit 。而且要搭配 :with 語句來控制 group-by 的行為。
範例:
    [:find ?i (avg ?r)
     :with ?u
     :where  [?e :movie/rating ?r]
                  [?e :movie/id ?i]
                  [?e :user/id ?u]]
相當於
select movie_id, avg(rating * 1.0) from RATINGS group by movie_id;

實際測試中,我用電影資料庫,放了 100000 筆的電影評分資料 (user id, movie id, rating)。結果用 Datomic query 做 group-by average 花費了 3000 milliseconds。但是 H2 database 卻只用了 500 milliseconds

( 註:做上述的實驗時,我對 Datomic 頗不熟,也因此沒有對 Datomic 設定 index 。然而, H2 database 卻有設定 index ,所以其實這個比較並不公平。由於電影資料的特性,其實可以考慮將 :/user/id:movie/id 這兩個 schema 都設定為 :db/unique )

(2) transact-async
本來我照範例是用 transact ,結果當有大量資料一開始要匯入時,資料庫就發生 timeout exception 了。查了論壇後,才發現因為 transact 函數內建的 timeout 是十秒。所以論壇上建議,遇到大量匯入或是資料庫在高負載時,要用 transact-async 。

If a system is under heavy load (e.g. an import job) then the default transaction timeout can be too short, and you should use transact-async so you can apply your own timeout logic.

Wednesday, September 13, 2017

clojure 實務技巧


  1. Application 讀取一般檔案
    使用 clojure.java.io/resource ,可以從 classpath 來讀取檔案。檔案放在 /resources 資料夾下即可。
  2. 「尋找 collections 符合某個條件的 item ,找到後傳回第一個」(find-first)
    stackoverflow 列舉了數種寫法 
    (a) filter/ first          -> 最容易理解
    (b) some
    (c) reduce/ reduced  -> 效能最好
  3. lein repl 啟動時發生 timeout
    解決方案: 在 project.clj 裡修改 repl 的 timeout
    :repl-options {
                 ;; If nREPL takes too long to load it may timeout,
                 ;; increase this to wait longer before timing out.
                 ;; Defaults to 30000 (30 seconds)
                 :timeout 120000
                 }
  4. 「讀取 linux 的 /proc 系統檔」
    需要使用一些特別的技巧,因為 /proc 下的檔案和一般的檔案不同,要先做一些轉換。
    (slurp (java.io.FileReader. "/proc/cpuinfo"))
  5. 將字串轉換成整數值
    儘量不要輕易地使用 read-string 這個函數。用比較單純的  (Integer/parseInt "123")
  6. here document
    clojure 沒有 here document 的語法。如果要使用的字串裡頭包含了太多需要逸脫 (escape) 的字元,官方建議的作法是把字串放在 /resources 下的檔案裡,用讀檔的方式取得。
  7. clojure 數值型態 suffix 的意義
    (type 1N)  => clojure.lang.BigInt
    (type 1)     => java.lang.Long
    (type 1.0)  => java.lang.Double
    (type 1M) => java.math.BigDecimal

Saturday, September 9, 2017

lexical binding v.s. dynamic binding

在 clojure 語言裡,變數可以是 lexical binding 也可以是 dynamic binding 。前者用於大多數的情況,因為這樣子寫,一眼就可以看出函數的依賴。後者多半用於一些 global 變數,而且是底層的函數才有在使用的 global 變數。這些 global 的變數,因為使用了 dynamic binding ,上層的函數就不需要一層又一層地去傳遞「變數的綁定」到最內層去。

範例在這邊:

javascript 的變數是採用 lexical binding 。但是, this 這個變數會隨著程式的運行,而指向不同的物件,this 顯然就是 dynamic binding 的變數。甚至 javascript 也提供了 bind 函數,可以用來明確地將函數與某個特定的物件做綁定。

有一種說法: 「Javascript 也可以算是一種 Lisp 的方言」。算不算是,自然是見仁見智。然而,如果我們從這個 lexical binding 和 dynamic binding 的角度來切入的話,確實是有特別的相似之處:因為都同時支持了兩種綁定方式。

Thursday, September 7, 2017

Data-driven programming 資料驅動程式設計

Data-driven programming ,出自 The Art of UNIX Programming 一書 。基本的概念是這樣子:

首先,人腦很不善長理解循序的「邏輯」。相反的是,人腦比較容易理解「資料」,怎樣算是資料呢?不管是表格、標記語言、巨集、樣板系統,這些都算是資料,都比循序邏輯容易理解。

於是,基於這個洞見,Unix 設計師使用它們的工具集:「高階語言、資料驅動程式設計、程式碼產生器、領域專用語言」來讓程式碼可以被極小化資料指定的規格所自動生成。

These insights ground in theory a set of practices that have always been an important part of the Unix programmer's toolkit — very high-level languages, data-driven programming, code generators, and domain-specific minilanguages. What unifies these is that they are all ways of lifting the generation of code up some levels, so that specifications can be smaller.

所謂資料驅動程式設計 (Data-driven programming) 和 OO 來做比較的話,主要有兩點不同:

在資料驅動程式設計裡,「資料」不僅只是物件的狀態,而是往往定義了程式的控制結構。 OO 的首要考量是「封裝」,而資料驅動程式設計首要的考量是「固定的程式碼」寫得愈少愈好。

In data-driven programming, the data is not merely the state of some object, but actually defines the control flow of the program. Where the primary concern in OO is encapsulation, the primary concern in data-driven programming is writing as little fixed code as possible.

書中是用 python 做為資料驅動程式設計的例子,但是 python 是 1990 才有的東西。

所以書裡有一段話描述了 1969 年的歷史:1969 年的 UNIX programmer 習慣於寫「語法解析器的規格」來生成「語法解析器」,好用來處理「標記語言」。因為做完語法解析器之後,剩下的工作就是對配置文件來做一般的「樹走訪」就可以完成了。要漂亮地解決問題,需要資料驅動程式設計的兩個階段來達成,而其中一個 (樹走訪) 建構於於另一個 (語法解析) 之上。

Unix programmers are very used to writing parser specifications to generate parsers for processing language-like markups; from there it was a short step to believing that the rest of the job could be done by some kind of generic tree-walk of the configuration structure. Two separate stages of data-driven programming, one building on the other, were needed to solve the design problem cleanly.

在 Data-driven programming 的概念下,程式不只是 Engine ,而且是 Data-programmable engine 。Data-driven programming 的經典實作品是 Ant 和 Interpreter。

Reflection/ Eval/ Lisp Macro

維基百科上的 Reflection 的定義:動態改變程式行為的能力,就算成是 reflection 。不過,實務上,大部分的高階程式語言的動態自我修改能力 (self-modify ability) ,我認為可以分成三個不同的等級。

(1)  Reflection
程式執行期間,利用外部來的字串來觸發函數。使用這個的話,可以減少一些重複的 switch 邏輯。
Clojure -> ns-resolve
Python  -> getattr
範例: // 現代的語言的標準函式庫幾乎都有類似的功能。
 
(2)  Eval
程式執行期間,增加程式本身 (program) 的功能。因為可以透過 Eval 定義新的函數。
 // Clojure, Python  辦得到。但是 Go 不行。

(3) Lisp Macro
程式執行期間,增加 interpreter 的功能。因為 macro 可以視為是一種 compiler plugin 。它可以增加程式語言本身的新的語法。此處所謂的新的語法,包含了 syntax 和 semantic 。 semantic 的部分,自然是改變了 interpreter 的行為才能辦到。
// Clojure, Hylang, Smalltalk 辦得到。

Wednesday, September 6, 2017

用 EDN 來取代 JSON

我在學習 clojure 的時候,有一個思考一陣子的問題:「程式語言之間的溝通傳輸的格式,要選用 JSON 還是 EDN 。如果使用 EDN 的話,會有什麼明確的好處嗎?」

考慮如下的例子:
要傳遞的資料,有「時間」的資料型態。而中間的傳輸格式,需要使用 rfc 3339
在 EDN 裡,時間可以直接儲存成 rfc 3339 的格式

 #inst "1985-04-12T23:20:50.52Z"

由於 EDN 已經有數種語言的實作可以用,下方是 python3 的範例。
如果傳送的資料格式是用 json 。而且傳遞的資料型態,恰好有一個是 rfc 3339 。 python 的 application logic 就會包含 (1)  json.loads() 和 (2) 一個特定的用來處理 rfc 3339 的邏輯。

與之相對的是,直接用 EDN format ,則是使用 edn_format.loads() 就可以搞定。

相差不多,只差一點點。後者可以讓 application logic 乾淨一點。因為減少了 context dependency 。所謂的 context dependency 就是指為了某些「json 沒有的 data type 」而花力氣去寫的 marshal/unmarshal logic 。

=================================================================

最後,那 EDN 到底跟 clojure 有什麼關系呢? EDN 的延伸資料型態 (extension elements),可以使用標記元素 (tagged element) 去表現。 比方說,rfc3339 就是一種內建的延伸資料型態。 uuid 則是另一種內建資料型態,長成這樣子: 

#uuid "f81d4fae-7dec-11d0-a765-00a0c91e6bf6"

使用者自訂的延伸資料型態,則是長成這個樣子:

#myapp/Person {:first "Fred" :last "Mertz"}

也因為格式的定義如此,每次要對 EDN 增加一種延伸的資料型態時,就是對 EDN 的 parser (每一種語言會有各自的實作) 寫出該種延伸資料型態的 parser plugin ,在 Clojure 的世界也可以稱之為 read macro ,再將這個 parser plugin 註冊到該種資料型態使用的標記元素之後, 這個 EDN parser 就可以無縫地運作。  marshal/unmarshal logic 也就可以完美地放進 parser 裡,不會混淆在 application logic 裡了。

What made lisp different?

Paul Graham 的文章 Waht made lisp different 真的好難懂。不過,我最近也寫過了 Clojure 的 macro ,疑似有理解一些了。

The whole language always available. There is no real distinction between read-time, compile-time, and runtime. You can compile or run code while reading, read or run code while compiling, and read or compile code at runtime.

這一段舉了四個例子來說明:
1. Running code at read-time lets user reprogram Lisp's syntax.
這是在講 Read macro 。
Common Lisp 的 read macro 是 reader 的 plugin ,可以註冊在 reader 裡。 而 Clojure 的話,內建了許多 read macro,但是不開放『自訂 read macro 』的功能。

2. Running code at compile-time is the basis of macros.
macro 可以視為是 compiler 的 plugin 。 在 Clojure 的例子是用 defmacro 。

3. Compiling at runtime is the basis of Lisp's use as an extension language in program like Emacs.
在 Clojure 的例子,使用 clojure.core/load , clojure.core/require 可以在執行期間啟動 compiler 。在 Riemann monitoring system 裡,就是利用 clojure.core/load 來讓 clojure 作為 Riemann 本身的 extension language 。而類似的 monitoring system 比方說像 Kapacitor 的話,則是還要特別定義一個專用的 extension language 。

4. Reading at runtime enables programs to communicate using s-expressions, an idea recently reinvented as XML.
在 Clojure 的例子,最接近的函數應該是 clojure.edn/read ,因為可以用於 untrusted source 。在 javascript 則是 JSON.parse() 。現代 web application 的前後端溝通所使用的 json 格式,也可以視為是一種 reading at runtime 特性的應用。