スレッディングマクロは、ネストした関数呼び出しを線形の関数呼び出しフローに変換し、コードの可読性を向上させる強力な機能です。
基本概念
assoc関数とupdate関数の理解
まず、スレッディングマクロを理解する前に、よく使用される基本的な関数について説明します。
関数 | 説明 | 使用例 |
---|---|---|
assoc |
マップに新しいキー・値のペアを追加 |
(assoc {:a 1} :b 2) → {:a 1, :b 2}
|
update |
マップの既存の値を関数で変更 |
(update {:age 30} :age inc) → {:age 31}
|
;; assocの例
(def person {:name "太郎"})
(assoc person :age 25)
;; => {:name "太郎", :age 25}
;; updateの例
(def person {:name "太郎", :age 30})
(update person :age inc) ; incは1を足す関数
;; => {:name "太郎", :age 31}
問題の発見:ネストした関数の読みにくさ
以下の関数を見てください:
(defn transform [person]
(update (assoc person :hair-color :gray) :age inc))
(transform {:name "ソクラテス", :age 39})
;; => {:name "ソクラテス", :age 40, :hair-color :gray}
この関数は以下の処理を行います:
-
person
に:hair-color :gray
を追加 - その結果の
:age
を1つ増やす
しかし、コードを読むときは内側から外側へ読む必要があり、直感的ではありません。
thread-first マクロ(->)
->
マクロは値を左から右に「スレッド」(通す)します。
基本的な使い方
(defn transform* [person]
(-> person
(assoc :hair-color :gray)
(update :age inc)))
動作原理
->
マクロは、各式の最初の引数として値を挿入します:
;; 通常の書き方
(defn transform* [person]
(-> person
(assoc :hair-color :gray)
(update :age inc)))
;; 挿入位置を,,,で示した場合(視覚的理解のため)
(defn transform* [person]
(-> person
(assoc ,,, :hair-color :gray) ; personが最初の引数に挿入
(update ,,, :age inc))) ; 前の結果が最初の引数に挿入
;; 実際に展開されるコード
(defn transform* [person]
(update (assoc person :hair-color :gray) :age inc))
段階的な実行の流れ
;; ステップバイステップの実行
(-> {:name "太郎", :age 30}
(assoc ,,, :hair-color :gray) ; ステップ1
(update ,,, :age inc)) ; ステップ2
;; ステップ1の実行結果
(assoc {:name "太郎", :age 30} :hair-color :gray)
;; => {:name "太郎", :age 30, :hair-color :gray}
;; ステップ2の実行結果
(update {:name "太郎", :age 30, :hair-color :gray} :age inc)
;; => {:name "太郎", :age 31, :hair-color :gray}
実践例
;; 単純な関数のチェーン
(-> {:name "田中"}
(assoc ,,, :age 25)
(assoc ,,, :city "東京")
(update ,,, :age #(+ % 5)))
;; => {:name "田中", :age 30, :city "東京"}
;; 実際の実行順序
;; 1. (assoc {:name "田中"} :age 25)
;; => {:name "田中", :age 25}
;; 2. (assoc {:name "田中", :age 25} :city "東京")
;; => {:name "田中", :age 25, :city "東京"}
;; 3. (update {:name "田中", :age 25, :city "東京"} :age #(+ % 5))
;; => {:name "田中", :age 30, :city "東京"}
;; キーワードや関数名だけの場合
(-> {:hair-color "black"} :hair-color name clojure.string/upper-case)
;; 挿入位置を示すと
(-> {:hair-color "black"}
(:hair-color ,,,) ; キーワード関数として実行
(name ,,,) ; 前の結果"black"を変換
(clojure.string/upper-case ,,,)) ; "BLACK"に変換
;; => "BLACK"
;; より複雑なデータ構造の操作
(-> {:user {:profile {:settings {:theme "dark"}}}}
(assoc-in ,,, [:user :profile :name] "太郎")
(update-in ,,, [:user :profile :settings] assoc :language "ja")
(get-in ,,, [:user :profile]))
;; => {:name "太郎", :settings {:theme "dark", :language "ja"}}
thread-last マクロ(->>)
->>
マクロは値を最後の引数として挿入します。
使用場面
シーケンス操作関数(map
, filter
, reduce
など)は通常、処理対象のシーケンスを最後の引数に取ります。
;; ネストした呼び出し(読みにくい)
(defn calculate []
(reduce + (map #(* % %) (filter odd? (range 10)))))
;; ->>を使用(読みやすい)
(defn calculate* []
(->> (range 10) ; [0 1 2 3 4 5 6 7 8 9]
(filter odd? ,,,) ; 挿入位置:最後の引数
(map #(* % %) ,,,) ; 挿入位置:最後の引数
(reduce + ,,,))) ; 挿入位置:最後の引数
;; 実際の展開結果
(defn calculate* []
(reduce + (map #(* % %) (filter odd? (range 10)))))
;; ステップバイステップの実行
;; 1. (range 10) => [0 1 2 3 4 5 6 7 8 9]
;; 2. (filter odd? [0 1 2 3 4 5 6 7 8 9]) => [1 3 5 7 9]
;; 3. (map #(* % %) [1 3 5 7 9]) => [1 9 25 49 81]
;; 4. (reduce + [1 9 25 49 81]) => 165
実践例
;; 単語のカウント
(->> ["apple" "banana" "apple" "cherry" "banana" "apple"]
(group-by identity ,,,)
(map (fn [[k v]] [k (count v)]) ,,,)
(into {} ,,,))
;; => {"apple" 3, "banana" 2, "cherry" 1}
;; 実行ステップ
;; 1. (group-by identity ["apple" "banana" "apple" "cherry" "banana" "apple"])
;; => {"apple" ["apple" "apple" "apple"], "banana" ["banana" "banana"], "cherry" ["cherry"]}
;; 2. (map (fn [[k v]] [k (count v)]) {上記の結果})
;; => (["apple" 3] ["banana" 2] ["cherry" 1])
;; 3. (into {} (["apple" 3] ["banana" 2] ["cherry" 1]))
;; => {"apple" 3, "banana" 2, "cherry" 1}
;; 数値処理の例
(->> [1 2 3 4 5 6 7 8 9 10]
(filter even? ,,,) ; 偶数のみ
(map #(* % 3) ,,,) ; 3倍する
(filter #(> % 10) ,,,) ; 10より大きいもの
(reduce + ,,,)) ; 合計
;; => 78
;; 実行ステップ
;; 1. (filter even? [1 2 3 4 5 6 7 8 9 10]) => [2 4 6 8 10]
;; 2. (map #(* % 3) [2 4 6 8 10]) => [6 12 18 24 30]
;; 3. (filter #(> % 10) [6 12 18 24 30]) => [12 18 24 30]
;; 4. (reduce + [12 18 24 30]) => 84
;; 文字列処理の例
(->> "Hello World From Clojure"
(clojure.string/split ,,, #" ")
(map clojure.string/lower-case ,,,)
(filter #(> (count %) 4) ,,,)
(clojure.string/join "-" ,,,))
;; => "hello-world-clojure"
どちらを使うべきか?
場面 | 使用するマクロ | 理由 |
---|---|---|
データ構造操作 | -> |
assoc , update , dissoc などは対象を最初の引数に取る |
シーケンス操作 | ->> |
map , filter , reduce などは対象を最後の引数に取る |
Java相互運用 | -> |
Javaオブジェクトは最初の引数として渡される |
;; データ構造操作の例(->を使用)
(-> {:name "山田"}
(assoc ,,, :age 30)
(update ,,, :age inc)
(dissoc ,,, :temp-field))
;; 挿入位置は常に最初の引数
;; シーケンス操作の例(->>を使用)
(->> [1 2 3 4 5]
(filter even? ,,,)
(map #(* % 2) ,,,)
(reduce + ,,,))
;; 挿入位置は常に最後の引数
;; Java相互運用の例(->を使用)
(-> "Hello World"
(clojure.string/lower-case ,,,)
(.replace ,,, "world" "clojure")
(.substring ,,, 0 5))
;; => "hello"
as->マクロ:柔軟な位置指定
場合によっては、値を異なる位置に挿入したい場合があります。
(as-> [:foo :bar] v
(map name v) ; vは最後の位置:(map name [:foo :bar])
(first v) ; vは最後の位置:(first ["foo" "bar"])
(.substring v 1)) ; vは最初の位置:(.substring "foo" 1)
;; => "oo"
;; 挿入位置を,,,で示すと
(as-> [:foo :bar] v
(map name ,,, v) ; 実際は(map name v)として実行
(first ,,, v) ; 実際は(first v)として実行
(.substring v ,,, 1)) ; 実際は(.substring v 1)として実行
詳細な実行ステップ
;; ステップバイステップの実行
(as-> [:foo :bar] v
(map name v) ; [:foo :bar] → ["foo" "bar"]
(first v) ; ["foo" "bar"] → "foo"
(.substring v 1)) ; "foo" → "oo"
;; より複雑な例:データ変換パイプライン
(as-> {:users [{:name "太郎" :age 30} {:name "花子" :age 25}]} data
(:users data) ; データからusersを取得
(map #(update % :age inc) data) ; 各ユーザーの年齢を+1
(filter #(> (:age %) 26) data) ; 26歳以上のユーザーのみ
(group-by #(> (:age %) 30) data) ; 30歳以上で分類
(get data true) ; 30歳以上のグループを取得
(mapv :name data)) ; 名前のみ取得
;; => ["太郎"]
;; 実際の実行ステップ
;; 1. (:users {:users [...]}) => [{:name "太郎" :age 30} {:name "花子" :age 25}]
;; 2. (map #(update % :age inc) [...]) => [{:name "太郎" :age 31} {:name "花子" :age 26}]
;; 3. (filter #(> (:age %) 26) [...]) => [{:name "太郎" :age 31} {:name "花子" :age 26}]
;; 4. (group-by #(> (:age %) 30) [...]) => {true [{:name "太郎" :age 31}], false [{:name "花子" :age 26}]}
;; 5. (get {...} true) => [{:name "太郎" :age 31}]
;; 6. (mapv :name [...]) => ["太郎"]
;; 文字列処理での混合位置指定
(as-> "Hello, Beautiful World!" text
(clojure.string/split text #" ") ; 文字列を分割
(map clojure.string/upper-case text) ; 各単語を大文字に
(clojure.string/join "-" text) ; ハイフンで結合 (textは2番目の位置)
(.replace text "BEAUTIFUL" "AMAZING") ; 置換 (textは1番目の位置)
(subs text 0 13)) ; 部分文字列 (textは1番目の位置)
;; => "HELLO-AMAZING"
実践例
(as-> "hello world" s
(clojure.string/split s #" ") ; ["hello" "world"]
(map clojure.string/upper-case s) ; ["HELLO" "WORLD"]
(clojure.string/join "-" s)) ; "HELLO-WORLD"
some->とsome->>:nil安全なスレッディング
nil
が途中で現れた場合に処理を停止します。
;; 従来の方法(冗長)
(when-let [counter (:counter a-map)]
(inc (Long/parseLong counter)))
;; some->を使用(簡潔)
(some-> a-map :counter Long/parseLong inc)
;; 挿入位置を示すと
(some-> a-map
(:counter ,,,) ; nilの場合ここで停止
(Long/parseLong ,,,) ; nilの場合ここで停止
(inc ,,,)) ; nilの場合ここで停止
詳細な動作例
;; 成功ケース
(some-> {:counter "42"}
(:counter ,,,) ; "42"を取得
(Long/parseLong ,,,) ; 42に変換
(inc ,,,)) ; 43にインクリメント
;; => 43
;; 失敗ケース1:キーが存在しない
(some-> {:other-key "value"}
(:counter ,,,) ; nilを返す → ここで停止
(Long/parseLong ,,,) ; 実行されない
(inc ,,,)) ; 実行されない
;; => nil
;; 失敗ケース2:パースエラー
(some-> {:counter "not-a-number"}
(:counter ,,,) ; "not-a-number"を取得
(Long/parseLong ,,,) ; 例外が発生するが、some->では捕捉されない
(inc ,,,))
;; => NumberFormatException
;; some->>の例
(some->> [1 2 3 4 5]
(map inc ,,,) ; [2 3 4 5 6]
(filter even? ,,,) ; [2 4 6]
(first ,,,)) ; 2
;; => 2
;; nilが混入した場合
(some->> nil
(map inc ,,,) ; 実行されない
(filter even? ,,,) ; 実行されない
(first ,,,)) ; 実行されない
;; => nil
実践例
;; 安全なチェーン処理
(some-> {:user {:profile {:name "太郎"}}}
(:user ,,,)
(:profile ,,,)
(:name ,,,)
(clojure.string/upper-case ,,,))
;; => "太郎"
;; 実行ステップ
;; 1. (:user {:user {:profile {:name "太郎"}}}) => {:profile {:name "太郎"}}
;; 2. (:profile {:profile {:name "太郎"}}) => {:name "太郎"}
;; 3. (:name {:name "太郎"}) => "太郎"
;; 4. (clojure.string/upper-case "太郎") => "太郎"
;; キーが存在しない場合
(some-> {:user {}}
(:user ,,,) ; {} (空のマップ)
(:profile ,,,) ; nil → ここで停止
(:name ,,,) ; 実行されない
(clojure.string/upper-case ,,,)) ; 実行されない
;; => nil (例外は発生しない)
;; 従来の書き方との比較
;; 従来の書き方(冗長)
(when-let [user (:user data)]
(when-let [profile (:profile user)]
(when-let [name (:name profile)]
(clojure.string/upper-case name))))
;; some->を使用(簡潔)
(some-> data :user :profile :name clojure.string/upper-case)
;; 実用的な例:APIレスポンスの処理
(defn extract-user-email [api-response]
(some-> api-response
(:data ,,,)
(:user ,,,)
(:contact ,,,)
(:email ,,,)
(clojure.string/lower-case ,,,)))
(extract-user-email {:data {:user {:contact {:email "USER@EXAMPLE.COM"}}}})
;; => "user@example.com"
(extract-user-email {:data {:user {}}}) ; emailが存在しない
;; => nil
cond->とcond->>:条件付きスレッディング
条件に基づいて処理を適用するかどうかを決定します。
(defn describe-number [n]
(cond-> []
(odd? n) (conj ,,, "奇数") ; 条件が真の場合のみ実行
(even? n) (conj ,,, "偶数") ; 条件が真の場合のみ実行
(zero? n) (conj ,,, "ゼロ") ; 条件が真の場合のみ実行
(pos? n) (conj ,,, "正数"))) ; 条件が真の場合のみ実行
(describe-number 3) ;; => ["奇数" "正数"]
(describe-number 4) ;; => ["偶数" "正数"]
(describe-number 0) ;; => ["偶数" "ゼロ"]
(describe-number -2) ;; => ["偶数"]
;; 実行の流れ(describe-number 3の場合)
;; 初期値: []
;; (odd? 3) => true → (conj [] "奇数") => ["奇数"]
;; (even? 3) => false → スキップ
;; (zero? 3) => false → スキップ
;; (pos? 3) => true → (conj ["奇数"] "正数") => ["奇数" "正数"]
詳細な実行例
;; cond->の詳細な動作
(cond-> {:name "太郎"}
true (assoc ,,, :processed true)
(= "太郎" (:name ,,,)) (assoc ,,, :japanese true) ; 注意:,,,は条件部では使えない
false (assoc ,,, :never-added true))
;; 正しい書き方
(let [person {:name "太郎"}]
(cond-> person
true (assoc :processed true)
(= "太郎" (:name person)) (assoc :japanese true)
false (assoc :never-added true)))
;; => {:name "太郎", :processed true, :japanese true}
;; cond->>の例
(cond->> [1 2 3 4 5]
true (map inc ,,,) ; [2 3 4 5 6]
(> 5 3) (filter even? ,,,) ; [2 4 6]
false (map #(* % 10) ,,,) ; スキップされる
true (reduce + ,,,)) ; 12
;; => 12
;; より実用的な例:設定に基づくデータ処理
(defn process-data [data {:keys [sort? reverse? limit]}]
(cond->> data
sort? (sort ,,,)
reverse? (reverse ,,,)
limit (take limit ,,,)))
(process-data [3 1 4 1 5 9 2 6] {:sort? true :reverse? true :limit 3})
;; => (9 6 5)
;; 実行ステップ
;; 1. sort? が true → (sort [3 1 4 1 5 9 2 6]) => [1 1 2 3 4 5 6 9]
;; 2. reverse? が true → (reverse [1 1 2 3 4 5 6 9]) => (9 6 5 4 3 2 1 1)
;; 3. limit が 3 → (take 3 (9 6 5 4 3 2 1 1)) => (9 6 5)
実践例
;; ユーザー情報の構築
(defn build-user [base-user admin? premium?]
(cond-> base-user
admin? (assoc ,,, :role :admin)
premium? (assoc ,,, :subscription :premium)
true (assoc ,,, :created-at (java.time.Instant/now))))
(build-user {:name "田中"} true false)
;; => {:name "田中", :role :admin, :created-at #inst"..."}
;; より複雑な例:レスポンス構築
(defn build-api-response [data {:keys [include-meta? include-debug? user-role]}]
(cond-> {:data data}
include-meta? (assoc ,,, :meta {:timestamp (System/currentTimeMillis)})
include-debug? (assoc ,,, :debug {:query-time "15ms"})
(= user-role :admin) (assoc ,,, :admin-info {:server "prod-1"})
(and include-meta? include-debug?) (assoc-in ,,, [:meta :debug-enabled] true)))
(build-api-response
{:users ["太郎" "花子"]}
{:include-meta? true :include-debug? true :user-role :admin})
;; => {:data {:users ["太郎" "花子"]},
;; :meta {:timestamp 1234567890, :debug-enabled true},
;; :debug {:query-time "15ms"},
;; :admin-info {:server "prod-1"}}
;; フォーム検証の例
(defn validate-form [form-data]
(cond-> {:valid? true :errors []}
(empty? (:name form-data)) (-> (assoc ,,, :valid? false)
(update ,,, :errors conj "名前は必須です"))
(< (count (:email form-data)) 5) (-> (assoc ,,, :valid? false)
(update ,,, :errors conj "メールアドレスが不正です"))
(< (:age form-data) 0) (-> (assoc ,,, :valid? false)
(update ,,, :errors conj "年齢は0以上である必要があります"))))
(validate-form {:name "" :email "a@b" :age -1})
;; => {:valid? false, :errors ["名前は必須です" "メールアドレスが不正です" "年齢は0以上である必要があります"]}
比較表:スレッディングマクロの使い分け
マクロ | 挿入位置 | 特徴 | 使用場面 |
---|---|---|---|
-> |
最初の引数 | 左から右へ読める | データ構造操作、Java相互運用 |
->> |
最後の引数 | 左から右へ読める | シーケンス操作 |
as-> |
任意の位置 | 変数名で参照 | 混合的な操作 |
some-> |
最初の引数 | nil安全 | nil の可能性がある操作 |
some->> |
最後の引数 | nil安全 | nil の可能性があるシーケンス操作 |
cond-> |
最初の引数 | 条件付き | 条件に応じた変換 |
cond->> |
最後の引数 | 条件付き | 条件に応じたシーケンス操作 |
ハンズオン練習
以下のコードを実際に試してみましょう:
練習1:基本的なデータ変換
;; 従来の書き方を->に変換してください
(update (assoc (dissoc {:name "太郎" :age 30 :temp true} :temp) :city "大阪") :age inc)
;; 答え(挿入位置を,,,で示す)
(-> {:name "太郎" :age 30 :temp true}
(dissoc ,,, :temp) ; 一時的なフィールドを削除
(assoc ,,, :city "大阪") ; 都市を追加
(update ,,, :age inc)) ; 年齢を1つ増やす
;; => {:name "太郎", :age 31, :city "大阪"}
;; 実行ステップ
;; 1. (dissoc {:name "太郎" :age 30 :temp true} :temp)
;; => {:name "太郎", :age 30}
;; 2. (assoc {:name "太郎", :age 30} :city "大阪")
;; => {:name "太郎", :age 30, :city "大阪"}
;; 3. (update {:name "太郎", :age 30, :city "大阪"} :age inc)
;; => {:name "太郎", :age 31, :city "大阪"}
練習2:シーケンス処理
;; 1から10までの数字から偶数を取り出し、それぞれを2乗して合計を求める
(->> (range 1 11)
(filter even? ,,,) ; 偶数のみフィルタ
(map #(* % %) ,,,) ; 各要素を2乗
(reduce + ,,,)) ; 合計を計算
;; => 220
;; より詳細な実行ステップ
;; 1. (range 1 11) => (1 2 3 4 5 6 7 8 9 10)
;; 2. (filter even? (1 2 3 4 5 6 7 8 9 10)) => (2 4 6 8 10)
;; 3. (map #(* % %) (2 4 6 8 10)) => (4 16 36 64 100)
;; 4. (reduce + (4 16 36 64 100)) => 220
;; 別の例:文字列リストの処理
(->> ["apple" "banana" "cherry" "date" "elderberry"]
(filter #(> (count %) 5) ,,,) ; 5文字以上の単語
(map clojure.string/upper-case ,,,) ; 大文字に変換
(map #(str "FRUIT: " %) ,,,) ; プレフィックス追加
(clojure.string/join ", " ,,,)) ; カンマ区切りで結合
;; => "FRUIT: BANANA, FRUIT: CHERRY, FRUIT: ELDERBERRY"
練習3:条件付き処理
;; ユーザーの権限に応じてメニューを構築
(defn build-menu [user]
(cond-> ["ホーム" "プロフィール"]
(:admin user) (conj ,,, "管理画面")
(:moderator user) (conj ,,, "モデレート")
(:premium user) (conj ,,, "プレミアム機能")
(:developer user) (conj ,,, "開発者ツール")))
(build-menu {:name "田中" :admin true :premium true})
;; => ["ホーム" "プロフィール" "管理画面" "プレミアム機能"]
;; 実行ステップ
;; 初期値: ["ホーム" "プロフィール"]
;; (:admin user) => true → (conj ["ホーム" "プロフィール"] "管理画面")
;; => ["ホーム" "プロフィール" "管理画面"]
;; (:moderator user) => nil (falsy) → スキップ
;; (:premium user) => true → (conj [...] "プレミアム機能")
;; => ["ホーム" "プロフィール" "管理画面" "プレミアム機能"]
;; (:developer user) => nil (falsy) → スキップ
練習4:as->を使った複雑な変換
;; JSONライクなデータの複雑な変換
(as-> {:products [{:name "ノートPC" :price 80000 :category "電子機器"}
{:name "マウス" :price 2000 :category "電子機器"}
{:name "本" :price 1500 :category "書籍"}]} data
(:products data) ; 商品リストを取得
(group-by :category data) ; カテゴリでグループ化
(update data "電子機器" #(filter (fn [p] (> (:price p) 5000)) %)) ; 高額電子機器のみ
(mapv (fn [[cat products]] ; カテゴリごとの統計作成
{:category cat
:count (count products)
:avg-price (/ (reduce + (map :price products)) (count products))}) data)
(sort-by :avg-price data)) ; 平均価格でソート
;; 実行ステップの詳細解説
;; 1. (:products {...}) => [{:name "ノートPC" ...} {...} {...}]
;; 2. (group-by :category [...]) => {"電子機器" [...] "書籍" [...]}
;; 3. 電子機器カテゴリから5000円以上の商品のみフィルタ
;; 4. 各カテゴリの統計情報を計算
;; 5. 平均価格でソート
練習5:some->による安全な操作
;; APIレスポンスからの安全なデータ抽出
(defn extract-user-info [api-response]
(some-> api-response
(:data ,,,)
(:user ,,,)
(:profile ,,,)
(select-keys ,,, [:name :email :age])
(update ,,, :name clojure.string/trim)
(update ,,, :email clojure.string/lower-case)))
;; 成功ケース
(extract-user-info
{:data {:user {:profile {:name " 太郎 " :email "TARO@EXAMPLE.COM" :age 30}}}})
;; => {:name "太郎", :email "taro@example.com", :age 30}
;; 失敗ケース(profileが存在しない)
(extract-user-info
{:data {:user {}}})
;; => nil
;; 失敗ケース(データ自体が存在しない)
(extract-user-info {})
;; => nil
;; 従来の書き方(冗長)
(defn extract-user-info-verbose [api-response]
(when-let [data (:data api-response)]
(when-let [user (:user data)]
(when-let [profile (:profile user)]
(-> profile
(select-keys [:name :email :age])
(update :name clojure.string/trim)
(update :email clojure.string/lower-case))))))
練習6:混合スレッディングマクロの実用例
;; eコマースサイトの商品フィルタリング機能
(defn filter-products [products {:keys [category min-price max-price sort-by desc?]}]
(cond->> products
category (filter #(= (:category %) category) ,,,) ; カテゴリフィルタ
min-price (filter #(>= (:price %) min-price) ,,,) ; 最低価格フィルタ
max-price (filter #(<= (:price %) max-price) ,,,) ; 最高価格フィルタ
sort-by (sort-by sort-by ,,,) ; ソート
desc? (reverse ,,,))) ; 降順にする場合
(def sample-products
[{:name "ノートPC" :price 80000 :category "電子機器"}
{:name "マウス" :price 2000 :category "電子機器"}
{:name "キーボード" :price 5000 :category "電子機器"}
{:name "小説" :price 1200 :category "書籍"}
{:name "技術書" :price 3500 :category "書籍"}])
(filter-products sample-products
{:category "電子機器"
:min-price 3000
:sort-by :price
:desc? true})
;; => ({:name "ノートPC", :price 80000, :category "電子機器"}
;; {:name "キーボード", :price 5000, :category "電子機器"})
;; 実行ステップ
;; 1. category "電子機器" でフィルタ => 3件
;; 2. min-price 3000 でフィルタ => 2件(マウスが除外)
;; 3. :price でソート => 価格昇順
;; 4. reverse で降順に => 価格降順
練習7:パフォーマンス比較
;; ネストした関数(読みにくい)
(defn process-nested [data]
(reduce +
(map #(* % %)
(filter odd?
(map inc
(range data))))))
;; ->>を使用(読みやすい)
(defn process-threaded [data]
(->> (range data)
(map inc ,,,)
(filter odd? ,,,)
(map #(* % %) ,,,)
(reduce + ,,,)))
;; 両方とも同じ結果
(process-nested 10) ;; => 165
(process-threaded 10) ;; => 165
;; 実行ステップ(process-threaded 10の場合)
;; 1. (range 10) => (0 1 2 3 4 5 6 7 8 9)
;; 2. (map inc (0 1 2 3 4 5 6 7 8 9)) => (1 2 3 4 5 6 7 8 9 10)
;; 3. (filter odd? (1 2 3 4 5 6 7 8 9 10)) => (1 3 5 7 9)
;; 4. (map #(* % %) (1 3 5 7 9)) => (1 9 25 49 81)
;; 5. (reduce + (1 9 25 49 81)) => 165
実践的なTips
1. 挿入位置の視覚化テクニック
開発中にコードの動作を理解しやすくするため、一時的に,,,
を使って挿入位置を明示できます:
;; デバッグ用の視覚化
(-> user-data
(assoc ,,, :timestamp (System/currentTimeMillis)) ; どこに挿入されるか明確
(update ,,, :visits inc) ; 前の結果が最初の引数
(dissoc ,,, :temp-fields)) ; さらに前の結果が最初の引数
2. スレッディングマクロの選択フローチャート
データ変換が必要?
├─ Yes
│ ├─ 常に最初の引数に挿入?
│ │ ├─ Yes
│ │ │ ├─ nil安全が必要?
│ │ │ │ ├─ Yes → some->
│ │ │ │ └─ No
│ │ │ │ ├─ 条件付き処理?
│ │ │ │ │ ├─ Yes → cond->
│ │ │ │ │ └─ No → ->
│ │ │ └─ No
│ │ │ ├─ 常に最後の引数に挿入?
│ │ │ │ ├─ Yes
│ │ │ │ │ ├─ nil安全が必要?
│ │ │ │ │ │ ├─ Yes → some->>
│ │ │ │ │ │ └─ No
│ │ │ │ │ │ ├─ 条件付き処理?
│ │ │ │ │ │ │ ├─ Yes → cond->>
│ │ │ │ │ │ │ └─ No → ->>
│ │ │ │ └─ No → as->(任意の位置)
└─ No → 通常の関数呼び出し
3. 読みやすさのベストプラクティス
;; 良い例:適切なインデント
(-> user
(assoc :created-at (now))
(update :age inc)
(dissoc :temp))
;; 悪い例:すべて1行
(-> user (assoc :created-at (now)) (update :age inc) (dissoc :temp))
;; 良い例:複雑な処理は分割
(->> data
(filter valid?)
(map transform)
(group-by :category)
(merge-with concat))
;; 良い例:コメントで各ステップを説明
(->> raw-data
(map parse-record) ; JSON文字列をパース
(filter valid-record?) ; 不正なレコードを除外
(group-by :date) ; 日付でグループ化
(transform-groups)) ; 各グループを集計
スレッディングマクロを使用することで、コードの可読性が大幅に向上し、データ変換のパイプラインを自然な順序で表現できるようになります。特に,,,
を使った挿入位置の視覚化は、初学者にとってマクロの動作を理解する上で非常に有効です。