我朋友 Karen ,她參與了一個 product manager 的線上社群,於是接手了一個 mentor-mentee 媒合的系統。她從事了 product manager 多年的工作,但是並沒有軟體開發的經驗。然而,她在對社群強烈的熱情趨使之下,硬是靠著 chatGPT 還是把 python 版本的 mentor-mentee 媒合系統給開發了出來。當然,故事沒有這麼簡單。故事的轉折點是,當她無論怎麼問 chatGPT 都在原地打轉時,她跑來問我。
她其中一個卡關的點是這樣子:她問 chatGPT 的問題,都是 top down 的問題,於是 chatGPT 就靠著機率與統計,去生成高階循序邏輯所對應的程式碼,也就是 policy 的部分。而這些生成的程式碼會需要呼叫遇到底層的 library 函數,也就是 mechanism 的部分,來做 bipartite matching 的計算。於是,chatGPT 就卡關了。像這種工作,就算是人類來做,有時候也得寫一些 glue code 來處理 policy code 與 mechanism code 之間的 impedance mismatch ,chatGPT 會卡關是可以預期的事。
我拿到 Karen 的專案之後,做了實驗、觀察、測試、調整,總之,我用 bottom up 的方式手寫了一段 glue code ,然後,Karen 就順利地抵達終點。
像前述這種 policy code 與 mechanism code 會 mismatch 的現象,我一直到了寫程式很多年之後,才勉強找到合適的詞彙來加以描述。另一方面,似乎是在很久以前,我就感受到了這種現象的存在。最初沒有想到什麼可行的解法,我選擇 bottom up 做為一切的終極解法。幾年後,我學到了 unit test 之後,總算覺得 top down 與 bottom up 同時使用的作法比較可行了,因為由上而下的程式碼要與由下而上的程式碼組裝在一起時,一旦有了單元測試,它就可以讓程式碼的行為變得可見,於是組裝程式碼、除錯、還有 feedback loop 都大幅縮減了。
身為 Clojure programmer ,我佔了一些不公平的優勢,很多在其它語言必須要透過 unit test 來處理的問題,對於我來講,REPL 一啟動之後就自動解決了。
延伸 glue code 的概念
如果仔細思考,當 policy code 要與 mechanism code 組合起來時,需要處理 impedance mismatch 。其實當我們要讓整個應用程式(application) 在不同的機器上執行、要更換資料庫的內容、更改資料的 schema、要變更某個 API 的版本時,不也是類似的問題嗎?
那我們該怎麼延伸 glue code 的概念到上述的這種情況?用類似的邏輯去思考的話,我們需要寫一些小程式,也就是管理進程 (admin process),它可以讓要下許多指令的工作可以自動化、讓指令執行完成的結果可以可視化、還有,它最好還可以與 Editor 整合在一起。
在 12 factor app 的 rule 12 有談論到管理進程,並且它主張,這些管理進程要跟常駐型的 process 有一模一樣的環境,最好用一樣的語言去開發,還要一起更新、發布,如此才能確保依賴隔離並且消除同步的問題。
在 Clojure 的生態系,由於 Clojure CLI 的直接啟動速度略慢,很多 Clojure 開發者選擇用 Babashka 來做管理進程--這不是什麼問題,因為要對 Babashka 做好依賴隔離不是什麼難事。
在 Gaiwan 的許多專案,我都可以找到一個有趣的小程式 bin/dev。它都叫同一個名字,只是,它的功能都不太一樣。這邊直接看一個 Hello World 的例子:
#!/usr/bin/env bb
(require
'[clojure.java.io :as io]
'[clojure.java.shell :as shell]
'[clojure.pprint :as pprint]
'[clojure.string :as str]
'[lambdaisland.cli :as cli]
'[lambdaisland.shellutils :as su]
)
(def init {})
(defn hello
"A shell hello function"
[opts]
(println (::cli/argv opts))
)
(def commands
["hello" #'hello])
(def flags
["-v, --verbose" "Increase verbosity"
"-n, --dry-run" "Show shell commands, but don't execute them"])
(cli/dispatch
{:init init
:commands commands
:flags flags})
如果你在 shell 裡執行 ./bin/dev hello a b c
的話,你可以得到輸出為 [a b c]
。
這邊的 bin/dev 是一個簡單的框架,你可以自訂你需要的函數,它的原理,你可以參考 Well Behaved Command Line Tool 。
管理進程可以減少認知負擔
管理進程可以設計成 one-off process 的形式,用來完成自動化的任務,舉凡:
bin/dev deploy
在遠端的機器上做 deploybin/dev generate-auth-keys
生成 authentication keysbin/dev up/down/clean
包裝 docker 的指令bin/dev tail-logs
登入遠端主機,看 logsbin/dev hosts
登入遠端主機,透過 Cloud 廠商的指令,取得主機狀態的資訊
每一家 Cloud 廠商總是提供不太一樣的指令,常用的指令先用管理進程包裝起來之後,即使過了一段時日之後早已經對這些指令不熟了,也不用再回去讀文件。而且,README 文件也可以寫得很簡單,因為一行的 bin/dev hosts
就可以取回現在正在運作的機器的狀態。
另一方面,也可以考慮設計成 REPL wrapper 的形式,比方說:
bin/dev prod-repl
登入遠端,並且連接上該主機的 socket REPLbin/dev mysql repl
啟動遠端資料庫的 REPL
每次要啟動 mysql 的 repl 時,如果總是要下像 mysql -u <local database username> -h <database server ip address> -p
的指令,就是有那麼一點心煩吧?把它包裝起來,多麼令人心情愉悅啊。
釋放不已的多巴胺
一個設計良好的 bin/dev
對於工程師的生產力,可以有巨大的貢獻。工程師如果只要讀完 README 還有利用 bin/dev
這樣子的管理進程 (admin process) 就可以流暢地工作,而不是寫一寫程式,遇上一些機器的問題就得停下來研究,腦子裡釋放不已的多巴胺也會快速地轉化為實質的工作進展。