0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Clojureのスレッディングマクロ [ 備忘録 ]

Posted at

スレッディングマクロは、ネストした関数呼び出しを線形の関数呼び出しフローに変換し、コードの可読性を向上させる強力な機能です。

基本概念

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}

この関数は以下の処理を行います:

  1. person:hair-color :grayを追加
  2. その結果の: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))       ; 各グループを集計

スレッディングマクロを使用することで、コードの可読性が大幅に向上し、データ変換のパイプラインを自然な順序で表現できるようになります。特に,,,を使った挿入位置の視覚化は、初学者にとってマクロの動作を理解する上で非常に有効です。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?