Clojure 集合 (Set) 的進階應用
集合與「基於 Primary Key 的唯一性」的語義差異
許多程式語言的容器只有「字典」與「陣列」,而 Clojure 語言還多了「集合」與「鏈列串列」。最近,我發現,「集合」容器還有一些語意的變化。
最近我在開發的 web 應用軟體,它的 http Session 裡的資料結構設計成如下:
{;; - explicitly track auth/credential challenges
;; - allow being logged in to multiple accounts
:authentications
{#uuid “identity-id” #{{:type “password”
:time #inst “...”}
{:type “totp”
:time #inst “...”}
{:type “backup_code”
:time #inst “...”}
{:type “magic_link”
:time #inst “...”}
{:type “otp”
:time #inst “...”}}
#uuid “other-identity” #{{:type “password” ,,,}}
}在其中 #{ ... } 是 Clojure 語言的集合容器,集合容器的語意是:「在容器之內的元素,不會有重複。」
然而,我真正需要語意是:「在容器之內的元素,對於每一種 type 的元素,不會有重覆。」換言之,如果把集合容器看成是資料表 (table),容器裡的每一個元素是一個 table row ,我需要的語意是:『primary key 是 type』。
元素重複
由於這兩個語意實在很相近,一開始大概我同事一度以為集合容器可以自動完成去除重複,所以不久之後,我就發現了集合容器內有多個元素是同一種 type 。
我的解法是,寫一個更新函數來封裝所有對該容器的修改操作,且在更新函數裡自動做『去除重複元素』的動作。
(defn update-session-auth! [session auth]
(let [{:keys [_ identity]} session
{:keys [type _]} auth]
(with-meta
(-> session
(update-in [:authentications identity]
#(set (remove (fn [a] (= (:type a) type)) %)))
(update-in [:authentications identity]
(fnil conj #{})
auth))
{:recreate true})))這個解法我覺得有點繞路。
還有什麼其它解法嗎?
1. 重新設計 http Session 裡的資料結構,用字典 (hashmap) 來取代 set ,並且用 type 做為字典的 key 值。
2. 似乎也可以考慮創造一種新的集合容器類別,它容許使用者指定 primary key 。



(require '[com.rpl.specter :as sp])
(defn update-session-auth! [session auth]
(sp/transform [:authentications (:identity session) (sp/nil->val #{})]
(fn [t] (->> t (remove #(= (:type %) (:type auth))) (cons auth) set))
session))