monoids について
monoids は、Clojure と ClojureScript のコードにモノイドの概念を適用するスキルです。マージや畳み込みにおける結合演算と単位元を見つけ、並列化できる場合も扱います。
主な機能
- 01結合演算と単位元の認識
- 02Clojure のコア関数をモノイドとして活用
- 03空コレクションを特別扱いせず処理
- 04結合律を利用した並列集約
- 05モノイドに該当しない演算の判定
活用シーン
- 01場当たり的なマージ処理を再利用可能な演算に置換
- 02空コレクションに対応する畳み込み
- 03結合律を満たす集約の並列化
- 04権限、カウンター、設定の合成モデル化
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ bsene/ skills --skill monoidsインストール後、Claude Code で/monoidsを入力して読み込まれたか確認します。
詳細
モノイド
モノイドは最小限の構造です。値の集合と、結合的な二項演算(combine)、そして単位元(empty)で構成されます。それだけです。この構造を業務コードの中で見つけると、場当たり的なマージ処理を、汎用的で一度テストすればよい API に置き換えられます。場当たり的な処理は、境界ケースでバグが発生する原因になりがちです。
このスキルでは Clojure/ClojureScript を扱います。概念は両方の言語で同じですが、慣用的な書き方が異なります。TypeScript の慣用的な書き方(インターフェース Monoid<T>、fold、業務モノイド)は、現在は typescript スキルの ../typescript/composition/SKILL.md(「Monoids」セクション)にあります。TS に関する質問は、こちらを参照するよう案内してください。
参考資料:モノイド:遍在する抽象化(evryg)
このスキルを適用する場面
注目すべきなのは「モノイド」という単語ではなく、依頼やコードに現れる次のパターンです。
- 特定の型に対して、
merge(a, b)、combine(a, b)、add(a, b)のような関数を手作業で実装している - 任意の初期値を使って、リストに対して
reduce/foldしている - 集約計算の前に
if (list.length === 0) throw / return nilがある - 集約計算を並列化したい(Map-Reduce、Kafka のパーティションごとの集約など)
- 権限、カウンター、ログ、設定など、自然に「足し合わせられる」業務モデルがある
こうしたパターンが1つでもあれば、ユーザーが名前を挙げるのを待たず、モノイド構造を積極的に提案してください。
Clojure/ClojureScript
Clojure では、この抽象化は暗黙的に扱われることがよくあります。この言語では、Monoid インターフェースを明示的な構造として定義するよりも、自然な単位元を持つ結合的な関数に対して reduce を使うのが一般的です。多くの core 関数(+、*、str、into、merge-with、concat、clojure.set/union)は、すでにモノイドです。これらの引数0個での呼び出しが、単位元を直接表します。
インターフェース(明示的に定義する必要がある場合)
(defrecord Monoid [empty combine])
(defn fold [{:keys [empty combine]} items]
(reduce combine empty items))
実際には、明示的に定義するより、以下の core 関数を使うのがおすすめです。より慣用的で、単位元もそのまま利用できます。
基本的なモノイド
;; Somme — (+) => 0, le neutre est intégré à l'arité 0
(reduce + [1 2 3]) ;; => 6
;; Concaténation de string
(apply str ["a" "b" "c"]) ;; => "abc"
;; Concaténation de collections
(reduce into [] [[1 2] [3 4]]) ;; => [1 2 3 4]
;; Booléens
(every? true? [true true false]) ;; and — neutre true
(some true? [false false true]) ;; or — neutre false
;; Composition de fonctions — (comp) => identity
(comp inc #(* % 2))
結合性 → 並列化
(defn parallel-fold [f init coll chunks]
(->> (partition-all (/ (count coll) chunks) coll)
(pmap #(reduce f init %))
(reduce f init)))
(parallel-fold + 0 (range 1000000) 4)
pmap は利用可能なコアに処理を分散します。f の結合性により、チャンクのグループ化を変えても結果は変わりません。分散モノイド fold と同じ仕組みです。
業務モデル
;; Panier — merge-with est LE combinateur idiomatique pour les maps
(merge-with + {:apple 2 :banana 1} {:apple 1 :cherry 3})
;; => {:apple 3, :banana 1, :cherry 3}
;; Permissions — clojure.set/union est associatif, #{} est le neutre
(require '[clojure.set :as set])
(apply set/union [#{:read} #{:write} #{:read :admin}])
;; => #{:read :write :admin}
;; Event log — simple concaténation, [] est le neutre
(apply concat [[:e1 :e2] [:e3] [:e4 :e5]])
;; => (:e1 :e2 :e3 :e4 :e5)
単位元 → 堅牢性
可変長引数の関数は単位元をネイティブに扱うため、「コレクションが空」のケースでも、特別な分岐なしで問題が起きません。
(reduce + []) ;; => 0, pas d'exception
(apply str []) ;; => ""
(merge-with + {}) ;; => {}
ここでは、引数0個での呼び出しが、関数のデフォルトの動作として単位元を返すことがよくあります。これは、その関数が代数を念頭に設計されたことを示すサインです。
合成可能性
;; 1. "Produit de monoïdes" — juxt applique plusieurs fonctions et combine leurs résultats
(defn count-and-sum [coll]
((juxt count (partial reduce +)) coll))
(count-and-sum [10 20 30]) ;; => [3 60]
;; 2. Fonctions X -> M — combiner ponctuellement deux fonctions vers un monoïde
(defn combine-fns [f g combine]
(fn [x] (combine (f x) (g x))))
;; 3. Map<K, monoïde> — merge-with généralise à n'importe quel combine
(merge-with into
{:errors [] :warnings [:w1]}
{:errors [:e1] :warnings [:w2]})
;; => {:errors [:e1], :warnings [:w1 :w2]}
;; Compteurs d'événements par type — pattern fréquent en event-driven
(defn count-events [events]
(reduce (fn [acc e] (merge-with + acc {(:type e) 1}))
{}
events))
Clojure 特有の注意点
init を指定しない (reduce f coll) は、明示的な単位元がなくても動作します。最初の要素が初期のアキュムレーターとして使われます。これは便利ですが、実際には単位元がないことを見落とすおそれがあります。ドメインに自然な単位元がない場合、それはモノイドではなく半群(単位元を持たない結合的な構造)です。並列化や空のケースの処理は、見た目以上にリスクが高くなります。
注意事項
- 演算が本当に結合的でない場合(例:減算、除算)は、抽象化を無理に適用しないでください。提案する前に法則を確認してください。簡単な反例を1つ挙げれば十分です。
- ドメインに自然な単位元がない場合、モノイドではなく半群(単位元を持たない結合的な構造)かもしれません。人工的な単位元を作るのではなく、そのことを伝えてください。
- 実用性を重視してください。目的は、場当たり的でバグのあるコードを汎用 API に置き換えることであり、博識に見せるために関数型の用語を押し付けることではありません。Clojure では、core 関数を使った
merge-with/reduceで十分なら、不要なMonoidの明示的な定義は提案しないでください。
ベンチマーク
シナリオ:.benchmarks/scenarios/monoids-001-parallel-merge.md · 実行:2026-08-31 · ログ:.benchmarks/runs/2026-08-31/monoids-001-parallel-merge.json
| モデル | なし | あり | 差分 |
|---|---|---|---|
| claude-opus-4-8 | 100% | 100% | +0% |
| claude-sonnet-4-6 | 83% | 100% | +17% |
| claude-haiku-4-5 | 100% | 100% | +0% |
ソフト合格(実行 2026-08-31)。差分がないことは想定どおりの結果です。フランス語の本文(
# Monoïdes)は、英語のプロンプトによる実行では有効になりません(監査警告)。Sonnet は +17(83→100)、opus/haiku は変化なしです。.agents/skills/skill-optimizer/rules/release-gates.mdに基づくゲートです。