在 Fennel 的官方文件裡,有一段話:
There's also a Cljlib library, that implements a lot of functions from clojure.core namespace and has a set of macros to make writing code more familiar to Clojure programmers
想要使用 Clojure 風格來寫程式的人,Cljlib 算是比較齊全的解決方案。更省事的方式,則是使用 nfnl 函式庫。由於 Conjure 裡也夾帶了 nfnl ,所以如果你之前在 day02 已經照步驟安裝了 Conjure ,那 nfnl 函式庫已經可以用了。
開啟之前下載的 auto-conjure 專案。
cd auto-conjure
建立一個 fnl/auto-conjure/temp.fnl
檔。
nvim fnl/auto-conjure/temp.fnl
將以下的內容貼入 temp.fnl
,並且對每個頂層的表達式用 ,ee
來求值。
(local core (require :conjure.nfnl.core))
(fn update-keys [t f]
"for every key in t, apply `(f key)`."
(->> (core.seq t)
(core.reduce (fn [acc v]
(core.assoc acc (f (core.first v)) (core.second v)))
{})))
(fn change-keyword [k]
{:key k})
;; (update-keys {:code "(+ 1 2)" :op :eval} change-keyword)
用 ,ee
求值時,得到的結果應該是類似於:
; eval (current-form): (local core (require :conju...
; eval (current-form): (fn update-keys [t f] "for ...
#<function: 0x01059a1a80>
; eval (current-form): (fn change-keyword [k] {:ke...
#<function: 0x01057ac5d8>
簡單解釋一下:
(local ... )
的傳回值為 nil
,所以看不到東西。(fn ...)
的傳回值都是非 nil
值,所以我們就可以在旁邊的視窗看到類似 #<function: 0x01057ac5d8>
的結果了。將游標移到 (13, 4) 的位置,即 ;; (update-keys
裡的左括弧位置,按下 ,ee
求值,一樣也會得到結果。
這邊有一個重點:「就算是註解,你還是可以對它進行求值。」很方便吧?註解即為測試。
這邊解釋一下程式碼在做什麼:
我定義了一個函數叫做
update-keys
。它可以對一個 Table 的所有鍵,用它的第二個引數的函數施加操作。
而在第 13 行的註解裡,
{:code "(+ 1 2)" :op :eval}
被施加操作之後,每一個鍵都會從字串變成 Table 。
鍵可以使用字串、數字以外的型態?沒錯,Lua 的 Table 就是提供這種語意 (semantics) 。
如果讀者去 Conjure 的 github repo 裡尋找 :conjure.nfnl.core
對應的源碼的話,你會找不到。因為在 Conjure 裡,它只放了已經被編譯成 Lua 的版本。
而 nfnl 有自己 repo ,我覺得比較好用的函式庫是以下這幾個:
在前面的範例裡的這段程式碼:
(fn update-keys [t f]
"for every key in t, apply `(f key)`."
(->> (core.seq t)
(core.reduce (fn [acc v]
(core.assoc acc (f (core.first v)) (core.second v)))
{})))
如果捨棄掉 nfnl 函式庫的話,會改成:
(fn update-keys [t f]
"for every key in t, apply `(f key)`."
(let [result {}]
(each [k v (pairs t)]
(tset result (f k) v))
result))
(fn update-keys [t f]
"for every key in t, apply `(f key)`."
(collect [k v (pairs t)]
(f k) v))
Clojure 函式庫的特色之一是:「幾乎每一個表示法都有傳回值。」在寫程式的時候,不需要想太多,反正就是都有傳回值。 此外,搭配了互動式開發之後,更是不太需要動腦,就是寫一點點程式,看傳回值變成什麼模樣,再一直調整,直到扺達終點。
所以,當我使用 reduce
時,傳入的匿名函數裡,我呼叫了 assoc
, 它也有傳回值,就是傳回修改完的 Table 。在此處,tset
的語意與 assoc
相似,都是修改 Table ,但是 test
的傳回值是 nil
,所以如果用了 tset
取代 assoc
的話,就會出錯。
沒辦法,誰叫 tset
是 Lua 的函數呢?
而 Fennel 畢竟也是設計成函數式編程語言,當然也要有函數式的寫法,所以會有 collect
。但是,骨子裡有一些微妙的不同: Fennel 提供的這些語法,都是巨集做成的;另一方面, :conjure.nfnl.core
的話,則是函數居多。
一方面,巨集不像函數那麼有豐富的表達能力,比方說,巨集不能做為函數的參數來傳遞。另一方面,正因為它是巨集,所以當這些程式碼被編譯成 Lua 之後,幾乎沒有什麼多餘的變數複製,非常節省資源,這可視為是一種效能最佳化。
Fennel 在設計時,考慮到嵌入式語言應用的情境,往往對於效能頗為計較,所以犧牲了些許的表達力來換取效能。換言之,如果不缺效能的時候,寫 Clojure 風格的 Fennel 也是可行的。