Saturday, November 1, 2025

Level up by diving down: eight critical Clojure libraries

之前在「我的 Clojure 學習之路」一文,我分析了自己過去學習的五個階段:

1. Before knowing Clojure
2. Clojure Basic
3. Tried a lot of libraries 
4. Selected Libraries
5. Level up by diving down

而在第五個階段,其實有一個很大的挑戰:「如果要 diving down 的話,要選什麼函式庫來 diving down ?」這個問題,透過 LLM 的協助,我整理出了以下的清單。這八個函式庫有以下的特性:

(a) 它們非常常用。如果你開發 web application 沒有用到它,八成是因為你用了替代方案。那你要不要質疑一下你的替代方案?
(b) 它們的設計根本就是經典。

很遺憾的事情是,我本人至今也還沒有研究完畢。我的學習過程一直沒有一套有系統的教材,是在前進之中思考合理的教材。

總之,清單如下:

   1. `weavejester/integrant`:

       * 角色: 應用程式生命週期管理、依賴注入 (DI)。

       * 啟發性: 教你如何將整個系統看作是資料。你用一個 map 來描述系統的所有元件(資料庫連線池、Web 伺服器、設定)以及它們之間的依賴關係。integrant 負責根據這個「藍圖」來啟動和關閉所有東西。這是「資料導向程式設計」在宏觀架構層面的終極體現。


   2. `juxt/aero`:

       * 角色: 設定管理。

       * 啟發性: 教你如何讓設定檔本身也變得可程式化。透過 EDN 的標籤 (#env, #ref),你可以建立出動態、可組合、且不會洩漏密碼的設定檔。它完美地展示了利用 Clojure Reader 的能力來解決一個普遍存在的問題。


   3. `ring-clojure/ring`:

       * 角色: HTTP 抽象層的基石。

       * 啟發性: Clojure 哲學的化身。它將複雜的 HTTP 請求/回應簡化為兩個 map,將橫切關注點(如日誌、Session)抽象為 middleware(高階函式)。研究 Ring,你就能理解組合性(composability) 的力量,以及如何用簡單的資料結構來為複雜的系統建立模型。


   4. `metosin/reitit`:

       * 角色: 路由 (Routing)。

       * 啟發性: 教你如何將路由表也視為資料。你用一個 vector of vectors 來定義整個 API 的端點、HTTP 方法、處理器以及中介軟體。這種資料導向的方法不僅性能極高,而且讓路由的組合、生成和內省 (introspection) 變得輕而易舉。它完美地展示了如何用資料結構來取代複雜的 DSL 或巨集。


   5. `metosin/malli`:

       * 角色: 資料規格定義、驗證與轉換 (Data Schema, Validation & Coercion)。

       * 啟發性: 教你如何用資料來描述資料的形狀。malli 讓你用 Clojure 的資料結構來定義你系統中所有資料的規格。這份單一的「規格資料」可以被多種方式使用:驗證 API 輸入、從字串自動轉換為 int 或 uuid、產生 OpenAPI/Swagger 文件、甚至進行生成式測試。它是一個典型的「一份定義,多種用途」的範例。


   6. `buddy`:

       * 角色: 身份驗證與授權。

       * 啟發性: `protocol` 和策略模式 (Strategy Pattern) 

         的最佳教材。它定義了一個抽象的驗證協議,然後允許你插入任何具體的驗證策略(Session, JWT Token 等)。它完美地展示了如何設計一個可擴展、可插拔的系統,將「做什麼」與「如何做」徹底分離。


   7. `seancorfield/honeysql`:

       * 角色: SQL 查詢產生器。

       * 啟發性: 再次展示了「將程式碼表示為資料」的思想。你不是在拼接字串,而是在建構描述 SQL 查詢的 Clojure map 和 vector。這使得動態產生複雜查詢變得極其安全和容易組合,徹底消除了 SQL 注入的風險。


   8. `seancorfield/next-jdbc`:

       * 角色: 資料庫存取。

       * 啟發性: 教你如何為 Java API 建立一個更符合 Clojure 習慣的介面卡 (Adapter)。它透過 protocol (ReadableColumn, SettableParameter) 讓你可以擴展它,使其能處理自訂的資料類型。同時,它的函數式、基於 Reducer 的資料處理方式也極具啟發性,展示了如何將 I/O 操作與純粹的資料轉換優雅地結合起來。

>>> 更新:由於我的老闆後來給了一個 talk (The Gaiwan Stack)
我有打算用 makina 來取代 integrant, 用 lambdaisland/config 來取代 aero, 還有另外再引入三個函式庫:
- lambdaisland/cli
- lambdaisland/ornament
- lambdaisland/hiccup

所以總共有 11 個 libraries。

Wednesday, July 16, 2025

Luarocks 與 Neovim

最近我遇到了一個挑戰:

> 如果使用 luarocks 安裝了 lua-cbor 的話,neovim 裡寫的 lua script 可以使用 lua-cbor 嗎?

答案是:預設不行,要做一些調整。

由於我的 luarocks 也是用 homebrew 去安裝的,而預設的 luarocks 它依賴的 lua interpreter 跟 neovim 預設依賴的 lua interpreter 不同。此外,neovim 的 package path 也很可能找不到 luarocks 的安裝 path 。

解法:
1. 重新安裝 luarocks ,下載 tar 包、重新編譯、安裝,讓它一定會依賴於 neovim 預設依賴的 lua interpreter ,即 luajit 
2. 設定 neovim 內部的 path ,使它可以讀得到 luarocks 的 module/library paths

< 重新安裝 luarocks 的指令 >
1. brew install luajit
2. wget https://luarocks.org/releases/luarocks-3.12.0.tar.gz
3. tar zxvf; cd luarocks-3.12.0
4. mkdir ~/.luarocks-luajit
5. ./configure \
  --with-lua=$(brew --prefix luajit) \
  --with-lua-include=$(brew --prefix luajit)/include/luajit-2.1 \
  --lua-suffix=jit \
  --prefix=$HOME/.luarocks-luajit
6. make && make install


注意:luarocks 的版本很重要,因為 luajit 有 65536 的限制。如果不是特定的 luarocks 版本,就會遇上這個錯誤:"Error: main function has more than 65536 constants"。參考連結。


< 設定 neovim 內部的 path >
1. 在 lua 資料夾下生成一個檔案  luarocks.lua
2. 貼上以下的內容:

```
local function add_luarocks_paths()

  local luarocks_path = "/Users/laurencechen/.luarocks-luajit/share/lua/5.1/?.lua;/Users/laurencechen/.luarocks-luajit/share/lua/5.1/?/init.lua"

  local luarocks_cpath = "/Users/laurencechen/.luarocks-luajit/lib/lua/5.1/?.so"

  package.path = package.path .. ";" .. luarocks_path

  package.cpath = package.cpath .. ";" .. luarocks_cpath

end

return {add_luarocks_paths = add_luarocks_paths}

```
3. 在 init.vim 裡增加一行 lua require("luarocks").add_luarocks_paths()

Sunday, December 22, 2024

neil --- 用來輔助編輯 deps.edn 檔的 CLI 工具

 neil 主要就四種常用的用法:

  1. 尋找函式庫  neil dep search clojure
  2. 新增函式庫  neil dep add org.clojure/clojure
  3. 新增 Clojure 專案 neil new app dev.replware/myproject
  4. 新增一些好用的輔助功能,比方說:test, nrepl, build (用來生成 uberjar) 等。
    neil add nrepl

其中,第一與第二種用法算是最常用的,因為 Clojure 的開發過程之中,就是會不停地尋找、新增 library 。而第三種用法裡,常用的有三種形式:

  • neil new app [web application 專案名稱]
  • neil new lib [library 專案名稱]
  • neil new scratch [免洗專案名稱]

Tuesday, December 10, 2024

使用 compojure 時,需要特別注意的一個 helper - wrap-route

compojure 在決定接下來要使用哪一個 route 是單純地一個一個逐步比對,這個單純的作法多數時候沒有問題,但是一旦當某個 route 的 middleware 會有副作用的時候,這個作法就會出問題。

所幸,compojure 後來提供了一個 wrap-route 恰好可以處理這種情況。

HoneySQL 的 format

HoneySQL format 的 default 輸出是給 JDBC 處理的,所以當想讓它輸出給 SQL CLI 時,就要使用參數。

  • 如果是使用 1.0 版本, format 的時候需要直接輸出可以被 SQL CLI 接受的字串形式,要使用 (format sql-hmap :parameterizer :none)
  • 如果是使用 2.0 版本, format 的時候需要直接輸出可以被 SQL CLI 接受的字串形式,要使用 (format sql-hmap {:inline true})

Tuesday, December 3, 2024

如何使用 assert ,它跟 exception 不一樣喔!

參考連結

有兩個很常見的 Exception handling 不良風格

1. 濫用 :pre 和 :post 來做檢查,而不是使用 spec 或 malli

2. catch Throwable

```

(try

(assert false)

(catch Throwable e

"Barfoo"))

```

理由是類似的:

(1) Error => 程式有 bug ,設計時不考慮 catch 它。這種可以用 assert 。
(2) Exception => 程式沒有 bug 但是執行的時候遇到異常,設計時可以考慮 catch 它。

註:

(a) 用 : pre 和 : post 的話,會丟出 Error ,但是一般的格式錯誤是 Exception
(b) catch Throwable 的話,會 catch 到 Error ,但是我們只應該 catch Exception ,不應該 catch Error 。

Protocol 與 SPI (service provider interface)

最近才搞懂的東西:「在什麼樣的 context 之下,應該考慮使用 Protocol ?」

為什麼在用了 Clojure 這麼多年之後才搞懂,應該是因為我過去寫程式的時間裡,幾乎沒有在寫 Java 。

我在 Clojure 官方論壇找到了答案

答案是:Protocol 最適合用於做為 SPI (service provider interface)

protocol functions are better as SPI to hook in implementations than in API as functions consumers call directly. It is often helpful to wrap protocol methods with a normal function that can supply additional logic if needed around the call into the protocol.

如何畫出 Datomic 的 schema diagram ?

與 SQL 相比,Datomic 這方面的工具比較少。並沒有像 dbdiagram 這種工具。

不過,有一個算是簡單好用的 workaround 方法:

1. 將 schema 搭配 chatGPT prompt 

> 將以下文本的 Datomic schema 的簡易表示法,轉換成 mermaid 的 ER diagram 表示法

2. 轉換完成之後,貼到 Mermaid Live Editor 來繪圖。

3. 做微調,因為有時候會判斷錯。

Wednesday, November 22, 2023

Clojure 取代 if 的技巧 (3) --- Atom validator

;; Using :validator to replace the `if`
(def a 
  (atom 3 :validator pos?))

(swap! a dec)

;; Without using `:validator`, we need to put a `if`
(def b
  (atom 3))

(swap! b (fn [n]
           (if (pos? (dec n))
             (dec n)
             (throw (ex-info "content must be positive"
                             {:a 1})))))


Wednesday, November 15, 2023

*out* 被 captured 時的變通之道

在 Clojure , *out* 是 System/out 這個變數被 java.io.OutputStreamWriter 的物件。

有的時候,有一些 codebase ,*out* 會被 captured ,這種時候,無論怎麼 print ,都無法 print 成功。這種時候,簡單的變通之道是:

(add-tap (fn [o] (.println System/out o)))
(tap> DATA)


Friday, October 27, 2023

Clojure 取代 if 的技巧 (2) --- cond->

問題:

有時候,我們會想要表達一種語意 (semantic)

if (c) {
  hm[x] = y
}

這種語意在 Clojure 會略顯得冗長

(if c
    (assoc hm :x y)
    hm)

但是,其實可以這樣子表達

(cond-> hm
   c (assoc :x y))







Clojure 取代 if 的技巧 (1) --- fnil

問題:
我們需要對一個 Clojure HashMap hm 裡的某個 path 做操作,插入 val 值。需要滿足的條件為:

  • 如果該 path 為空,就插入空的 vector,並且在該 vector 裡插入 val 
  • 如果該 path 已經有一個 vector  ,就把 val 插入 vector 的尾端
直覺的寫法可能是:

(if (nil? (get-in hm path)
    (assoc-in hm path [val])
    (update-in hm path conj val))

但是,這又略顯得重複、冗長

解法:
  • (update hm path (fnil conj []) val)
  • (update-in hm path (fnil conj []) val)


Clojure 的 tree 表示法

我本來是比較喜歡 (2) ,多想了一陣子之後,又覺得 (1) 似乎更好一些些。

使用 (1) 的表示法還有一個理由: 假設,你現在是要從一堆已經 serialized data 裡,一個又一個的 item 去讀取,最後會建出這顆樹。很有可能,你會想寫 recursive function 。

而在 Clojure 裡,想到 recursive function ,要想到兩種改善效能的方式:
(1) recur
(2) lazy-seq

其中,lazy-seq 的話,需要跟 cons 一起搭配使用,所以 (1) 的表示法相對有彈性。

Tuesday, August 15, 2023

malleable design

malleable design 又可以稱之為 evolvable design 。舉例子來說明的話:

  • Lisp Macro: 提供你一個 compiler plugin
  • Postgres UDF (user defined function) / Object system / foreign data interface
  • Protocol, atom semantic in Clojure

上述這些可以延展系統的接口設計,都可以讓後人巧妙地去增加原本的抽象層的廣度與深度。如果善用這些接口的話,日後使用系統的人,可以充分地享受 conceptual integrity 的好處,而不是得到一個層層疊床架屋才勉強構築出來的系統。

雖然我寫 Clojure 也好一陣子了,對於『延展 Clojure』的技巧,一直沒有充分地掌握。仔細想想的話,這個才是 Clojure 最 powerful 的技巧之一,真的要講的話,像大名鼎鼎的 Reagent 就是用這種技巧做出的 library 。使用 reagent 的感覺非常奇妙:一方面,你知道你在寫的東西,它的底層是 React 。另一方面,你又可以使用 Clojure 的高階 semantic ,直接用 @ operator 來取得 reagent 內部的資料。 

Thursday, June 29, 2023

background job queue library of Clojure

先談簡易的結論:
1. 想搭配 Postgres 使用 => 用 Proletarian
2. 想搭配 Redis 使用 => 用 Jesque 
3. 都不滿意的話,就再去 Java 的 ecosystem 找,總會找到勉強可以接受的 library 來用。


其實這個問題也滿多人討論的:
比方說,這一篇:有人在 ClojureVerse 詢問,在 Clojure 世界裡,相當於 Ruby 的 sidekiq 是什麼?


< 技術決策的理論 >

如果是我來思考這個議題的話,兩個重點:

1. Deployment 是否省事?
2. Library 提供給我的 interface 是否好用?直覺?不會太多儀式性的 code 要寫

DevOps 要省事的話,常常會不想用 Redis 。可以參考這一篇。


由於使用 Redis 做為 durable storage 的 library 疑似發展比較久、也比較成熟,有可能反而是 Redis 組別的 Library interface 比較好用。類似的觀點來自這邊。


< 技術決策的實務 >

Proletarian 與 Jesque 我都用過,library 給的感覺嘛,好像也沒有差很多。大概還是優先選 Proletarian ,畢竟我本人很怕 Deploy 那一段的 complexity。

Postgres 的一些好用的功能:Auth, jsonb, validation on json, ltree, Expression based index

1. Postgres 可以利用 pg_crypto 這個 extension ,快速地做出 Authentication。參考這篇。

2. Postgres 的 jsonb column ,這可以用來快速地搬移 Firestore  到 Postgres 。參考這篇。

3. Postgres 可以對 json column 加上 schema validation。參考這篇。

4. Postgres 可以利用 ltree 這個 extension 來記錄 tree 類型的資料結構。參考這篇。

5. Postgres 也支援 expression based unique index 。參考這篇。

第五項多加一些說明:

在 Clojure 的世界,我想找一個簡單易用的 worker queue ,但是又懶得架 Redis,所以 Proletarian 是選項之一。(Proletarian 是一個輕薄的 job queue library on top of Postgres)

然而,使用 Proletarian 也是有一些缺點:

- 比方說,job 的 payload 會被 Proletarian library 先做成 Transit ,再轉換成 text 來儲存。這時,如果你要放進 worker queue 裡的 job ,你想要對其施加一些 unique constriaint 時,該怎麼做呢?


之前我想不出做法,因為整個 payload 都被先打包成一包做成 text ,總覺得 unique constraint 加不上去。無奈之下,只好在 application layer 去實作這個 unique constraint 的檢查,實在有夠難寫。


最近研究 Postgres 才發現,有一個很犯規的東西叫做 expression based UNIQUE INDEX。用了這個的話,就可以輕易地對 text 或是 json 之類的資料的部分集合,加上 unique constraint了。 

Thursday, February 16, 2023

Web application 生成的報表速度太慢

 最近在 Gaiwan 公司遇到客戶委任我們做一些效能改進:

  1. 客戶把 code 的 link 給我之後,他們留下的訊息是說,覺得有沒有可能,把一些 code 改成用 Transducer 來改寫,效能就會好了。
  2. 我第一時間就覺得,八成不是這個問題。一般來講,web application 這種東西,會慢,十之八九都是資料庫的查詢在慢,更何況,客戶用的資料庫還是 Datomic 。
  3. 但是,光是有直覺沒有用,還是要設法証明。我很久沒有做 profiling 了,上次做疑似是十年前,總之,基本的概念還有,技術完全沒有。花了一些時間研究工具之後,發現有兩個程式很有用。
    (3.a) jcmd => 這個可以快速找到 java program 對應的啟動 argument 與 pid
    (3.b) VisualVM => 我是用 statistical mode 去量測。它可以幫你一層又一層地向下展開,看到 x function calls y function ,且 x function 的 total time, CPU time, self time 。我用了這個之後,層層展開函數呼叫後,果然,定位出來,最耗時的地方是資料庫查詢 (Database Query)
  4. 該怎麼改進呢? 一旦找到關鍵點了,要改就有很多可能性。在眾多的改法之中,我個人覺得工程略為耗大,但是一勞永逸的改法,是把資料 sync 到資料倉儲 (Data warehouse) 裡,把本來很慢的資料庫查詢都送到資料倉儲去。由於資料倉儲在 schema 、index 上都有許多神奇的最佳化,效能的大躍進滿可以期待的。
  5. 然而,這件事的結尾並不是使用資料倉儲。
    我老闆一眨眼就找出效能低落的關鍵點:N+1 problem 。速度很慢的那一段程式碼,它在迴圈裡做 Query 。如果 Query 是那種極度單純的,完全不需要 scan 資料庫的,老實說還好,畢竟 Datomic 在 peer 端做了大量的 cache ,並不會有很大的 I/O 開銷。 而我遇到的這個案例,迴圈裡的 Query 是有做一些些 unification 的。所以解決之道,是要改變程式碼的寫法,把 Query 移到迴圈外。 修改完之後,就快了 20 倍。

Thursday, February 9, 2023

Clojure syntax error 解決方案

使用 Clojure CLI 來做為啟動程式的話,由於 Clojure CLI 會生成 .cpcache 檔,啟動速度會比較快一點。然而,有的時候,.cpcache 可能有一些 cache 造成程式的載入順序錯誤,就會造成 syntax error 

如果遇到明明沒有寫錯 syntax ,卻一直得到 syntax error 的話,可以考慮將 .cpcache 刪除,就可以解決了。

Thursday, June 30, 2022

tmux, Clojure REPL, direnv, alias

開發 Clojure/ClojureScript 的程式時,我的習慣是會用 tmux 來開啟一個在背景的 repl 來做 development 

我常用的一些 shell alias 長成這樣子:

alias bbrepl="tmux new-session -d -s bbrepl 'bb nrepl-server 1667'"

alias drepl="tmux new-session -d -s drepl 'clj -M:dev:cider-clj'"

alias dfull="tmux new-session -d -s dfull 'clj -M:dev:shadow-cider-clj'"

alias dshadow="tmux new-session -d -s dshadow 'clj -M:shadow-cljs watch main'"


這個方式用了很久都沒有什麼問題,直到有一天,我發覺我需要所謂的 local repl 也就是說,我啟動的這個 repl ,它指定的 deps.edn 的 alias 是可以根據 project directory 來變動的。


本來,我設法用了 direnv ,很快就做到了,「可以讓 environment variable 隨著切換進入資料夾而掛載」。然而,其實我需要的是「可以讓 alias 隨著切換進入資料夾而掛載」。


最後我的作法是這樣子:
(1) 在專案的資料夾下,準備一個 .alias 的資料夾,裡頭放一些我要拿來當指令的 shell script ,比方說 lrepl

(2) 在專案的資料夾下,準備一個 .envrc 檔,裡頭寫了 
PATH_add .alias

Monday, May 2, 2022

在 mac 電腦安裝/使用 pyenv 

安裝 pyenv

step 1: 利用 homebrew 來安裝

brew update
brew install pyenv

step 2: 設置 zshell

echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.zshrc
echo 'command -v pyenv >/dev/null || export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.zshrc
echo 'eval "$(pyenv init --path)"' >> ~/.zshrc

pyenv 的基本操作指令:

(1) pyenv versions
顯示現在 local 的電腦有哪些 python 版本可以使用
(2) pyenv install --list
顯示 pyenv 提供哪些版本的 python 可供安裝
(3) pyenv install 3.7.0 
安裝某特定 python 版本
(4) pyenv global 3.70 
將系統預設的 python 設定成 3.7.0
(5) pyenv local 3.7.0 
將當前 session 的 python 設定成 3.7.0

參考資料