iT邦幫忙

2025 iThome 鐵人賽

DAY 7
0

在 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 函式庫已經可以用了。

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 求值,一樣也會得到結果。

這邊有一個重點:「就算是註解,你還是可以對它進行求值。」很方便吧?註解即為測試。

Table 的鍵也可以是 Table 型態

這邊解釋一下程式碼在做什麼:

我定義了一個函數叫做 update-keys 。它可以對一個 Table 的所有鍵,用它的第二個引數的函數施加操作。

而在第 13 行的註解裡,{:code "(+ 1 2)" :op :eval} 被施加操作之後,每一個鍵都會從字串變成 Table 。

鍵可以使用字串、數字以外的型態?沒錯,Lua 的 Table 就是提供這種語意 (semantics) 。

源碼在哪?

如果讀者去 Conjure 的 github repo 裡尋找 :conjure.nfnl.core 對應的源碼的話,你會找不到。因為在 Conjure 裡,它只放了已經被編譯成 Lua 的版本。

而 nfnl 有自己 repo ,我覺得比較好用的函式庫是以下這幾個:

  • core:這是精簡版的仿 Clojure 標準函式庫。
  • notify:log 函式庫。
  • macros:提供了 time 函數。

Clojure 風格的寫法

在前面的範例裡的這段程式碼:

(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))

表達能力 vs 效能

Clojure 函式庫的特色之一是:「幾乎每一個表示法都有傳回值。」在寫程式的時候,不需要想太多,反正就是都有傳回值。 此外,搭配了互動式開發之後,更是不太需要動腦,就是寫一點點程式,看傳回值變成什麼模樣,再一直調整,直到扺達終點。

所以,當我使用 reduce 時,傳入的匿名函數裡,我呼叫了 assoc , 它也有傳回值,就是傳回修改完的 Table 。在此處,tset 的語意與 assoc 相似,都是修改 Table ,但是 test 的傳回值是 nil,所以如果用了 tset 取代 assoc 的話,就會出錯。

沒辦法,誰叫 tset 是 Lua 的函數呢?

而 Fennel 畢竟也是設計成函數式編程語言,當然也要有函數式的寫法,所以會有 collect 。但是,骨子裡有一些微妙的不同: Fennel 提供的這些語法,都是巨集做成的;另一方面, :conjure.nfnl.core的話,則是函數居多。

一方面,巨集不像函數那麼有豐富的表達能力,比方說,巨集不能做為函數的參數來傳遞。另一方面,正因為它是巨集,所以當這些程式碼被編譯成 Lua 之後,幾乎沒有什麼多餘的變數複製,非常節省資源,這可視為是一種效能最佳化

小結

Fennel 在設計時,考慮到嵌入式語言應用的情境,往往對於效能頗為計較,所以犧牲了些許的表達力來換取效能。換言之,如果不缺效能的時候,寫 Clojure 風格的 Fennel 也是可行的。


上一篇
Fennel 語言速成 -- LuaRocks
下一篇
Lisp 深入淺出 -- 互動式開發
系列文
在 Neovim 中探索 Fennel 與函數式編程9
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言