基本的な概念
:keys
はClojureの分割束縛 - destructuringで使用される特別なキーワードです。関数の戻り値そのものではなく、マップから特定のキーの値を抽出するための構文です。
:keys
の動作原理
;; 元のマップ
(def person {:name "田中" :age 30 :city "東京"})
;; :keysを使った分割束縛
(let [{:keys [name age]} person]
(println name age))
;; => 田中 30
視覚的な説明図
マップの構造:
┌─────────────────────────────────┐
│ {:name "田中" │
│ :age 30 │
│ :city "東京"} │
└─────────────────────────────────┘
│
▼
{:keys [name age]}
│
▼
┌─────────────────────────────────┐
│ name → "田中" │
│ age → 30 │
└─────────────────────────────────┘
使用例とパターン
1. 関数の引数での使用
(defn greet [{:keys [name age]}]
(str "こんにちは、" name "さん(" age "歳)"))
(greet {:name "佐藤" :age 25})
;; => "こんにちは、佐藤さん(25歳)"
2. let束縛での使用
(let [data {:x 10 :y 20 :z 30}
{:keys [x y]} data]
(+ x y))
;; => 30
3. 関数の戻り値を分割束縛
(defn get-coordinates []
{:x 100 :y 200})
(let [{:keys [x y]} (get-coordinates)]
(println "座標:" x y))
;; => 座標: 100 200
重要なポイント
-
:keys
自体は値を返さない - 分割束縛の構文 - 元のマップから値を抽出 - キー名と同じ変数名を作成
- 存在しないキーは
nil
になる
(let [{:keys [name email]} {:name "山田"}]
(println name email))
;; => 山田 nil
他の分割束縛オプション
-
:keys
- キーワードキー用 -
:strs
- 文字列キー用 -
:syms
- シンボルキー用
;; 文字列キーの場合
(let [{:strs [name age]} {"name" "鈴木" "age" 35}]
(println name age))
;; => 鈴木 35
:keys
と:as
の組み合わせ
:keys
で値を抽出しつつ、元のマップ全体も保持したい場合は:as
を使います。
基本的な使用法
(defn process-user [{:keys [name age] :as params}]
(println "名前:" name)
(println "年齢:" age)
(println "全体のパラメータ:" params))
(process-user {:name "田中" :age 30 :city "東京"})
;; => 名前: 田中
;; => 年齢: 30
;; => 全体のパラメータ: {:name "田中", :age 30, :city "東京"}
視覚的な説明図
元の引数: {:name "田中" :age 30 :city "東京"}
│
├─────────────────┐
│ │
▼ ▼
:keys で抽出 :as で元のまま保持
│ │
▼ ▼
┌─────────────────────────────────────────────┐
│ 抽出された個別の値: │
│ name → "田中" │
│ age → 30 │
│ │
│ 元の引数そのまま: │
│ params → {:name "田中" :age 30 :city "東京"} │
│ ↑ │
│ 完全に同じオブジェクト │
└─────────────────────────────────────────────┘
重要なポイント
:as
は分割束縛の結果ではなく、元の引数をそのまま参照します
(defn demo [{:keys [name] :as original}]
(println "抽出した値:" name)
(println "元の引数:" original)
(println "同じオブジェクト?" (identical? original {:name "田中" :age 30 :city "東京"})))
;; 引数として渡されたマップと:asで束縛された値は同一
(let [input-map {:name "田中" :age 30 :city "東京"}]
(demo input-map))
実用的な例
(defn validate-and-process [{:keys [name email] :as user-data}]
(cond
(empty? name) {:error "名前が必要です" :data user-data}
(empty? email) {:error "メールが必要です" :data user-data}
:else {:success true :user user-data :greeting (str "こんにちは " name "さん")}))
(validate-and-process {:name "佐藤" :email "sato@example.com" :phone "090-1234-5678"})
;; => {:success true,
;; :user {:name "佐藤", :email "sato@example.com", :phone "090-1234-5678"},
;; :greeting "こんにちは 佐藤さん"}
let束縛での使用
(let [config {:host "localhost" :port 3000 :debug true}
{:keys [host port] :as full-config} config]
(println "接続先:" host ":" port)
(when (:debug full-config)
(println "デバッグモード有効 - 設定:" full-config)))
ネストした分割束縛での使用
(defn process-request [{:keys [user params] :as request}]
(let [{:keys [name id] :as user-info} user
{:keys [action data] :as request-params} params]
{:processed-by name
:user-id id
:action action
:original-request request}))
分割束縛とLazy Sequenceの関係
重要:分割束縛は必要な部分だけlazy sequenceを実現(realize)します
Lazy Sequenceでの分割束縛
;; 無限のlazy sequence
(defn infinite-numbers []
(iterate inc 0))
;; 分割束縛で最初の3つだけを取得
(let [[a b c] (infinite-numbers)]
(println a b c))
;; => 0 1 2
;; 無限シーケンスだが、必要な3つだけが計算される
視覚的な説明図
Lazy Sequence: [0 1 2 3 4 5 6 ...∞]
│ │ │
▼ ▼ ▼ (必要な分だけ実現)
[a b c] の分割束縛
│
▼
┌─────────────────────────────┐
│ a → 0 (実現済み) │
│ b → 1 (実現済み) │
│ c → 2 (実現済み) │
│ │
│ 残りは未実現のまま │
└─────────────────────────────┘
実例での動作確認
;; 副作用のあるlazy sequence(計算のタイミングを確認)
(defn debug-seq []
(map (fn [x]
(println "計算中:" x)
x)
(range 10)))
;; 分割束縛で最初の2つだけ使用
(let [[first second] (debug-seq)]
(println "結果:" first second))
;; 出力:
;; 計算中: 0
;; 計算中: 1
;; 結果: 0 1
;; → 必要な2つだけが計算された!
マップの分割束縛は即座に実現
;; マップの場合は即座にキーにアクセス
(defn create-map-with-lazy-vals []
{:a (do (println "aを計算") 1)
:b (do (println "bを計算") 2)
:c (delay (do (println "cを計算(遅延)") 3))})
(let [{:keys [a b]} (create-map-with-lazy-vals)]
(println "使用:" a b))
;; 出力:
;; aを計算
;; bを計算
;; 使用: 1 2
;; → :cは使わないが、マップ作成時に:aと:bは計算される
ネストしたシーケンスでの分割束縛
;; ネストしたlazy sequenceの場合
(defn nested-lazy []
(map (fn [x]
(println "外側計算:" x)
(map (fn [y]
(println "内側計算:" y)
(* x y))
(range 5)))
(range 3)))
;; 最初の要素の最初の2つだけを取得
(let [[[a b] & rest] (nested-lazy)]
(println "結果:" a b))
;; 出力:
;; 外側計算: 0
;; 内側計算: 0
;; 内側計算: 1
;; 結果: 0 0
パフォーマンスの考慮事項
;; 大きなlazy sequenceでの効率的な分割束縛
(defn process-large-data []
(->> (range 1000000)
(map #(* % %))
(filter even?)))
;; 最初の10個だけを処理
(let [[a b c & more] (take 10 (process-large-data))]
(println "最初の3つ:" a b c)
(println "残り:" (count more)))
;; → 100万個全てではなく、必要な分だけが計算される
まとめ
分割束縛は確かにlazy sequenceを必要な分だけ実現します:
- シーケンスの分割束縛 → 必要な要素数だけ計算
- マップの分割束縛 → キーへの即座アクセス(既に実現済みの値)
- ネストした構造 → 必要な深さまで段階的に計算
- パフォーマンス → メモリ効率が良い(全体を実現しない)
これがClojureの遅延評価の強力な特徴の一つです。