Clojure에서의 구조 분해(Destructuring)
Clojure에서의 구조 분해(Destructuring)
코드 안에서 값 하나하나를 일일이 꺼내 바인딩하다 보면, 정작 하고 싶은 일이 무엇인지가 잘 보이지 않게 돼요. 구조 분해는 데이터 구조 안에 들어 있는 값들에 이름을 간결하게 붙이는 방법인데요, 이걸 쓰면 코드가 훨씬 짧아지고 읽기 쉬워져요.
가장 익숙한 벡터 예시부터 살펴볼게요. 아래처럼 값을 하나씩 꺼내 이름을 붙이는 코드를 생각해 보죠.
(def my-line [[5 10] [10 20]])
(let [p1 (first my-line)
p2 (second my-line)
x1 (first p1)
y1 (second p1)
x2 (first p2)
y2 (second p2)]
(println "Line from (" x1 "," y1 ") to (" x2 ", " y2 ")"))
;= "Line from ( 5 , 10 ) to ( 10 , 20 )"
이 코드는 그 자체로는 완벽하게 동작해요. 다만 값을 꺼내고 이름을 붙이는 일에 코드가 너무 많이 쓰여서, 우리의 의도가 묻혀 버리죠. 구조 분해를 쓰면 복잡한 데이터 구조에서 중요한 부분을 더 짧고 깔끔하게 꺼낼 수 있어요.
;= 위와 같은 벡터를 사용
(let [[p1 p2] my-line
[x1 y1] p1
[x2 y2] p2]
(println "Line from (" x1 "," y1 ") to (" x2 ", " y2 ")"))
;= "Line from ( 5 , 10 ) to ( 10 , 20 )"
변수 하나하나를 명시적으로 바인딩하는 대신, 값들이 순서대로 어디에 있는지를 설명해 주는 거예요. [[5 10] [10 20]]처럼 생긴 my-line이라는 구조가 있을 때, 구조 분해 형식으로 "두 개의 원소를 가진 벡터"라고 그 구조를 적어 주면, [5 10]은 p1에, [10 20]은 p2에 들어가요. 그리고 p1과 p2의 구조 자체가 아니라 그 안의 원소를 다루고 싶으니, 같은 let 안에서 p1과 p2를 다시 구조 분해해요. p1이 [5 10]이니까 두 원소 x1, y1로 나타내면 5는 x1에, 10은 y1에 바인딩되죠. p2도 마찬가지로 10은 x2에, 20은 y2에 들어가요.
여기서 한 걸음 더 나아가면, p1이나 p2를 위한 바인딩을 아예 만들지 않고 재귀적으로 구조 분해하는 훨씬 간결한 버전도 쓸 수 있어요.
;= 위와 같은 벡터를 사용
(let [[[x1 y1] [x2 y2]] my-line]
(println "Line from (" x1 "," y1 ") to (" x2 ", " y2 ")"))
;= "Line from ( 5 , 10 ) to ( 10 , 20 )"
본문
순차 구조 분해(Sequential Destructuring)
Clojure의 구조 분해는 크게 두 갈래로 나뉘는데요, 순차 구조 분해와 연관 구조 분해가 그것이에요. 순차 구조 분해는 let 바인딩 안에서 순차적인 데이터 구조를 Clojure 벡터로 나타내는 방식이에요.
이 방식은 리스트, 벡터, seq, 문자열, 배열처럼 선형 시간에 순회할 수 있는 모든 데이터 구조 — 즉 nth를 지원하는 것은 무엇이든 — 에 쓸 수 있어요.
(def my-vector [1 2 3])
(def my-list '(1 2 3))
(def my-string "abc")
;= 놀랍지 않게도 1 2 3이 출력된다
(let [[x y z] my-vector]
(println x y z))
;= 1 2 3
;= 리스트도 비슷한 방법으로 구조 분해할 수 있다
(let [[x y z] my-list]
(println x y z))
;= 1 2 3
;= 문자열은 문자 단위로 구조 분해된다
(let [[x y z] my-string]
(println x y z)
(map type [x y z]))
;= a b c
;= (java.lang.Character java.lang.Character java.lang.Character)
순차 구조 분해의 핵심은 값들을 하나씩 벡터 안의 심볼에 매칭시키는 거예요. 예를 들어 [x y z]라는 벡터는 '(1 2 3)이라는 리스트의 각 원소와 하나씩 짝을 지어요.
구조 분해하려는 컬렉션의 크기가 바인딩 개수와 정확히 같지 않을 때가 있어요. 벡터가 너무 작으면 남는 심볼은 nil에 바인딩돼요. 일반적으로 바인딩 형식이 값과 매칭되지 않으면 그 바인딩은 nil이 된답니다.
(def small-list '(1 2 3))
(let [[a b c d e f g] small-list]
(println a b c d e f g))
;= 1 2 3 nil nil nil nil
반대로 컬렉션이 너무 크면, 남는 값은 그냥 무시돼요.
(def large-list '(1 2 3 4 5 6 7 8 9 10))
(let [[a b c] large-list]
(println a b c))
;= 1 2 3
구조 분해는 어떤 원소를 바인딩할지(혹은 하지 않을지), 어떻게 바인딩할지를 완전히 제어하게 해 줘요. 컬렉션의 모든 원소가 필요한 경우는 드물고, 특정 원소만 필요한 경우가 많죠.
(def names ["Michael" "Amber" "Aaron" "Nick" "Earl" "Joe"])
첫 번째 원소를 한 줄에, 나머지를 다른 줄에 출력하고 싶다고 해 볼게요.
(let [[item1 item2 item3 item4 item5 item6] names]
(println item1)
(println item2 item3 item4 item5 item6))
;= Michael
;= Amber Aaron Nick Earl Joe
이 바인딩도 동작하지만 구조 분해를 썼는데도 꽤 투박하네요. 이럴 때 &를 쓰면 꼬리 부분의 원소들을 하나의 시퀀스로 묶을 수 있어요.
(let [[item1 & remaining] names]
(println item1)
(apply println remaining))
;= Michael
;= Amber Aaron Nick Earl Joe
쓰지 않을 바인딩이 있다면 아무 심볼로 바인딩해서 무시할 수도 있어요. 관례상 밑줄(_)을 쓰면 되죠.
(let [[item1 _ item3 _ item5 _] names]
(println "Odd names:" item1 item3 item5))
;= Odd names: Michael Aaron Earl
:as all을 쓰면 전체 벡터를 all이라는 심볼에 바인딩할 수 있어요.
(let [[item1 :as all] names]
(println "The first name from" all "is" item1))
;= The first name from [Michael Amber Aaron Nick Earl Joe] is Michael
잠깐 멈추고 :as와 &의 타입을 좀 더 자세히 들여다볼게요.
(def numbers [1 2 3 4 5])
(let [[x & remaining :as all] numbers]
(apply prn [remaining all]))
;= (2 3 4 5) [1 2 3 4 5]
여기서 remaining은 numbers 벡터에서 남은 원소들로 이루어진 시퀀스에, all은 원래 벡터에 바인딩돼요. 그럼 문자열을 구조 분해하면 어떨까요?
(def word "Clojure")
(let [[x & remaining :as all] word]
(apply prn [x remaining all]))
;= \C (\l \o \j \u \r \e) "Clojure"
여기서 all은 원래 구조(문자열이든 벡터든 리스트든 무엇이든)에, x는 문자 \C에, remaining은 나머지 문자들의 리스트에 바인딩돼요.
이 기법들은 원하는 대로 동시에 조합해서 쓸 수도 있어요.
(def fruits ["apple" "orange" "strawberry" "peach" "pear" "lemon"])
(let [[item1 _ item3 & remaining :as all-fruits] fruits]
(println "The first and third fruits are" item1 "and" item3)
(println "These were taken from" all-fruits)
(println "The fruits after them are" remaining))
;= The first and third fruits are apple and strawberry
;= These were taken from [apple orange strawberry peach pear lemon]
;= The fruits after them are (peach pear lemon)
구조 분해는 중첩해서 쓸 수도 있어서, 순차 구조의 임의의 깊이에 접근할 수 있어요. 처음에 봤던 my-line 벡터로 돌아가 볼게요.
(def my-line [[5 10] [10 20]])
이 벡터는 직접 접근할 수 있는 중첩 벡터로 이루어져 있어요.
(let [[[x1 y1][x2 y2]] my-line]
(println "Line from (" x1 "," y1 ") to (" x2 ", " y2 ")"))
;= "Line from ( 5 , 10 ) to ( 10 , 20 )"
중첩된 벡터가 있을 때는 :as나 &도 어느 단계에서든 쓸 수 있어요.
(let [[[a b :as group1] [c d :as group2]] my-line]
(println a b group1)
(println c d group2))
;= 5 10 [5 10]
;= 10 20 [10 20]
연관 구조 분해(Associative Destructuring)
연관 구조 분해는 순차 구조 분해와 비슷하지만, 맵, 레코드, 벡터 등을 포함한 연관(키-값) 구조에 적용한다는 점이 달라요. 연관 바인딩은 맵에서 키로 값을 간결하게 추출하는 데 초점을 맞춰요.
먼저 구조 분해 없이 맵에서 값을 꺼내는 예시를 볼게요.
(def client {:name "Super Co."
:location "Philadelphia"
:description "The worldwide leader in plastic tableware."})
(let [name (:name client)
location (:location client)
description (:description client)]
(println name location "-" description))
;= Super Co. Philadelphia - The worldwide leader in plastic tableware.
let 바인딩의 각 줄이 본질적으로 같다는 걸 눈치채셨죠. 키 이름으로 맵에서 값을 꺼내서, 같은 이름의 로컬에 바인딩하고 있어요.
아래는 연관 구조 분해로 같은 일을 하는 첫 예시예요.
(let [{name :name
location :location
description :description} client]
(println name location "-" description))
;= Super Co. Philadelphia - The worldwide leader in plastic tableware.
구조 분해 형식이 이제 벡터가 아니라 맵이 됐어요. let 왼쪽에 심볼 대신 맵이 오는데, 이 맵의 키가 let 안에 바인딩할 심볼이고, 맵의 값이 연관 값에서 조회할 키예요. 여기서는 키워드(가장 흔한 경우)를 썼지만, 숫자, 문자열, 심볼 등 어떤 키 값이든 될 수 있어요.
순차 구조 분해와 비슷하게, 맵에 없는 키를 바인딩하려고 하면 그 값은 nil이 돼요.
(let [{category :category} client]
(println category))
;= nil
다만 연관 구조 분해는 :or 키로, 키가 연관 값에 없을 때 쓸 기본값을 제공할 수 있어요.
(let [{category :category, :or {category "Category not found"}} client]
(println category))
;= Category not found
입력 맵에 없는 키에 기본값을 주려면 :or 지시어를 쓰면 돼요. :or의 값은 바인딩 이름(또는 [1.13부터] 키)과 기본값의 맵이에요. 여기서는 :category 키가 입력 맵(client)에 없으니 "Category not found"가 category 바인딩의 값으로 쓰여요. 참고로 :or 항목은 다른 곳에서 언급된 바인딩과 키를 가리키는 게 좋아요.
:or 맵에 적어 둔 기본값들을 나중에 코드에서 다시 쓰지 않고 캡처하고 싶다면, :defaults 지시어([1.13부터])를 쓸 수 있어요. 이건 구조 분해 중에 만들어진 "키 → 기본값" 맵을 어떤 이름에 바인딩해 줘요.
(let [{category :category, :or {category "Category not found"}, :defaults df} client]
(println df))
;= {:category "Category not found"}
순차 구조 분해에서는 안 쓰는 값을 보통 _로 바인딩하죠. 연관 구조 분해는 전체 구조를 순회할 필요가 없으니, 쓰지 않을 키는 구조 분해 형식에서 그냥 빼 버리면 돼요.
전체 맵에 접근해야 한다면, 순차 구조 분해처럼 :as 키로 들어온 전체 값을 바인딩할 수 있어요.
(let [{name :name :as all} client]
(println "The name from" all "is" name))
;= The name from {:name Super Co., :location Philadelphia, :description The world wide leader in plastic table-ware.} is Super Co.
:as, :or, :defaults 키워드는 하나의 구조 분해 형식 안에서 함께 쓸 수 있어요.
(def my-map {:a "A" :b "B" :c 3 :d 4})
(let [{a :a, x :x, :or {x "Not found!"}, :as all, :defaults df} my-map]
(println "I got" a "from" all)
(println "Where is x?" x)
(println "Defaults are" df))
;= I got A from {:a "A" :b "B" :c 3 :d 4}
;= Where is x? Not found!
;= Defaults are {:x "Not found!"}
아까 원래 예시가 여전히 중복 정보(로컬 바인딩 이름과 키 이름)를 담고 있다는 걸 눈치채셨을 거예요. :keys 키를 쓰면 이 중복을 더 줄일 수 있어요.
(let [{:keys [name location description]} client]
(println name location "-" description))
;= Super Co. Philadelphia - The worldwide leader in plastic tableware.
이 예시는 앞선 버전과 정확히 같아요. name은 (:name client)에, location은 (:location client)에, description은 (:description client)에 바인딩해요.
:keys는 키가 키워드인 연관 값용이고, 문자열 키와 심볼 키에는 각각 :strs와 :syms가 있어요. 이 모든 경우에 벡터 안에는 로컬 바인딩 이름이 되는 심볼들이 들어가요.
(def string-keys {"first-name" "Joe" "last-name" "Smith"})
(let [{:strs [first-name last-name]} string-keys]
(println first-name last-name))
;= Joe Smith
(def symbol-keys {'first-name "Jane" 'last-name "Doe"})
(let [{:syms [first-name last-name]} symbol-keys]
(println first-name last-name))
;= Jane Doe
특정 키가 없는 맵이 들어오면 nil이나 기본값 대신 오류를 내고 싶다면, [1.13부터] 지시어의 검사 버전(:keys!, :strs!, :syms!)을 쓸 수 있어요. 필요 키가 없으면 예외를 던지죠.
(let [{:strs! [first-name last-name dob]} string-keys]
(println first-name last-name))
;= throws Missing required key: "dob"
필수로 검사만 하고 바인딩은 하고 싶지 않은 키가 있다면 & 뒤에 나열하면 돼요. [1.13부터] 이렇게 하면 나열된 키의 존재는 검사하되 바인딩은 만들지 않아요.
(let [{:keys! [fred ethel lucy & :ricky]} m] ...
(let [{:strs! [fred ethel lucy & "ricky"]} m] ...
(let [{:syms! [fred ethel lucy & 'ricky]} m] ...
또한 &를 써서 (:keys, :strs, :syms) 지시어 안에서 바인딩하지 않는 추가 선택 키들을 문서화할 수도 있어요.
입력 맵을 받을 때 그 안에 정확히 뭐가 들었는지 알 수 없는 경우가 많아요. 이 불확실성을 다루려고 Clojure 개발자들은 입력 맵이 받아들이는 합법적인 키 집합을 고정하곤 하는데, 이러면 시스템 경계를 가로질러 취약한 연결이 생겨나요. 다루는 키가 무엇인지, 혹은 추가 키를 받아들이지 못하는 무언가에 데이터를 넘겨야 할 때도 입력을 받는 쪽은 유연하게 하고 싶다면, :select([1.13부터])를 쓸 수 있어요. :select 지시어는 입력 맵의 부분 집합에 기본값이 더해진, 구조 분해 형식에 언급된 키들로 제한된 맵에 이름을 붙여 줘요.
(def small-map {:a 5})
(let [{:keys [a b & :c], :as m, :or {a 42 b 3 :c "unknown"}, :select sel, :defaults df} small-map]
(println sel))
;= {:c "unknown", :b 3, :a 5}
입력 맵처럼 생겼지만 빠진 키에 기본값이 대신 들어간 맵을 얻으려면 :all 지시어를 쓰세요. [1.13부터] :all은 중첩된 맵을 깊이 탐색해요.
(let [{:keys [a & :b] :as m :or {:a 42 :b 3} :all all-m} {}]
[a all-m])
-> [42 {:a 42, :b 3}]
:all을 쓰면 :or 지시어가 있는 모든 중첩 단계에 기본값이 적용돼요.
(let [nested-map {:a 1, :nested {:aa 10}}
{:keys [a & :b] :as m :or {:a 42 :b 3} :all all-m
{:keys [aa bb] :or {:bb 30}} :nested}
nested-map]
all-m)
-> {:a 1, :b 3, :nested {:bb 30, :aa 10}}
연관 구조 분해는 중첩하거나 필요에 따라 순차 구조 분해와 조합할 수 있어요.
(def multiplayer-game-state
{:joe {:class "Ranger"
:weapon "Longbow"
:score 100}
:jane {:class "Knight"
:weapon "Greatsword"
:score 140}
:ryan {:class "Wizard"
:weapon "Mystic Staff"
:score 150}})
(let [{{:keys [class weapon]} :joe} multiplayer-game-state]
(println "Joe is a" class "wielding a" weapon))
;= Joe is a Ranger wielding a Longbow
키워드 인자(Keyword arguments)
한 가지 특별한 경우로, 키워드 인자를 파싱하는 데 연관 구조 분해를 쓰는 경우가 있어요. :debug와 :verbose 옵션을 받는 함수를 생각해 보죠. 이 옵션들은 옵션 맵으로 지정할 수 있어요.
(defn configure [val options]
(let [{:keys [debug verbose] :or {debug false, verbose false}} options]
(println "val =" val " debug =" debug " verbose =" verbose)))
(configure 12 {:debug true})
;;val = 12 debug = true verbose = false
하지만 이 선택 인자들을 그냥 추가적인 "키워드" 인자처럼 넘길 수 있다면 입력하기 더 편하겠죠.
(configure 12 :debug true)
이런 호출 방식을 지원하기 위해, 연관 구조 분해는 키워드 인자 파싱을 위한 키-값 쌍의 리스트나 시퀀스에서도 동작해요. 이 시퀀스는 가변 인자 함수의 rest 인자에서 오는데, 순차 구조 분해가 아니라 연관 구조 분해로 분해돼요(그래서 맵 안의 키-값 쌍인 것처럼 시퀀스를 분해해요).
(defn configure [val & {:keys [debug verbose]
:or {debug false, verbose false}
:as opts}]
(println "val =" val " debug =" debug " verbose =" verbose " opts =" opts))
(configure 10)
;;val = 10 debug = false verbose = false opts = nil
(configure 5 :debug true)
;;val = 5 debug = true verbose = false opts = {:debug true}
;; 키워드 인자의 순서는 상관없음을 참고
(configure 12 :verbose true :debug true)
;;val = 12 debug = true verbose = true opts = {:verbose true, :debug true}
키워드 인자는 주로 REPL에서 사람들이 직접 타이핑하는 인터페이스나 API의 가장 바깥쪽 계층을 만들 때 쓰여요. 일반적으로 코드의 안쪽 계층에서는 옵션을 명시적인 맵으로 넘기는 게 더 쉬워요. 그런데 Clojure 1.11부터는 키워드 인자를 기대하는 함수에 대체되는 키→값 쌍, 혹은 그런 매핑들의 맵, 심지어 그 앞에 키→값을 가진 맵까지 넘길 수 있는 기능이 추가됐어요. 그래서 위의 configure 호출은 앞서 본 형태들에 더해 아래 형태도 모두 받을 수 있어요.
(configure 12 {:verbose true :debug true})
;;val = 12 debug = true verbose = true
(configure 12 :debug true {:verbose true})
;;val = 12 debug = true verbose = true
키워드 인자를 기대하는 함수에 넘기는 마지막 맵은 키→값 쌍으로 제공된 기본 키들을 덮어쓸 때 자주 유용해요.
네임스페이스가 있는 키워드(Namespaced keywords)
맵의 키가 네임스페이스가 있는 키워드라도, 로컬 바인딩 심볼에는 네임스페이스를 붙일 수 없다는 점만 빼면 구조 분해를 쓸 수 있어요. 네임스페이스가 있는 키를 구조 분해하면 키의 네임스페이스 부분은 버리고 로컬 이름 부분에 값이 바인딩돼요. (그래서 네임스페이스가 없는 키와 마찬가지로 :or을 쓸 수 있어요.)
(def human {:person/name "Franklin"
:person/age 25
:hobby/hobbies "running"})
(let [{:keys [hobby/hobbies]
:person/keys [name age]
:or {age 0}} human]
(println name "is" age "and likes" hobbies))
;= Franklin is 25 and likes running
네임스페이스가 있는 키워드를 :keys만으로 구조 분해하면 로컬 바인딩이 충돌할 수 있어요. 구조 분해의 모든 옵션을 조합할 수 있으니, 어떤 로컬 바인딩 형식이든 각각 따로 정의할 수 있어요.
(def human {:person/name "Franklin"
:person/age 25
:hobby/name "running"})
(let [{:person/keys [age]
hobby-name :hobby/name
person-name :person/name} human]
(println person-name "is" age "and likes" hobby-name))
;= Franklin is 25 and likes running
심지어 자동 해석 키워드(auto-resolved keyword) 로 구조 분해할 수도 있는데, 이 경우에도 키의 이름 부분만 바인딩돼요.
;; 프로젝트에 person.clj 네임스페이스가 있다고 가정
;; 없으면 REPL에서 다음과 같이 하면 된다: (create-ns 'person) (alias 'p 'person)
(require '[person :as p])
(let [person {::p/name "Franklin", ::p/age 25}
{:keys [::p/name ::p/age]} person]
(println name "is" age))
;= Franklin is 25
자동 해석 키워드로 맵을 만들고 구조 분해하면, 현재 네임스페이스의 require로 정의된 네임스페이스 별칭(여기서는 p)을 쓰는 코드를 작성할 수 있어요. 이렇게 하면 코드의 한 곳에서 바꿀 수 있는 네임스페이스 간접화(indirection) 수단이 생겨요.
구조 분해 문맥에서 바인딩된 모든 심볼은 다시 구조 분해될 수 있어서, 순차 구조 분해든 연관 구조 분해든 중첩 방식으로 쓸 수 있어요. 덜 명확하지만 이건 & 뒤에 정의되는 심볼에도 똑같이 적용돼요.
이 예시는 rest 인자를 옵션으로 해석하기 위해 & 시퀀스를 그 자리에서 구조 분해해요(주의: 그래서 두 인자는 순차적으로, rest는 연관적으로 분해하고 있어요).
(defn f-with-options
[a b & {:keys [opt1]}]
(println "Got" a b opt1))
(f-with-options 1 2 :opt1 true)
;= Got 1 2 true
어디에서 구조 분해를 쓸까
구조 분해는 명시적이든 암시적이든 let 바인딩이 있는 모든 곳에서 쓸 수 있어요.
가장 흔한 곳 중 하나는 함수에 전달된 인자를 분해하는 경우예요. "let x는 이것, let y는 저것…" 식의 표준 코드가 여기 있죠. 다시 말하지만 완전히 유효한 코드지만 장황해요.
(defn print-coordinates-1 [point]
(let [x (first point)
y (second point)
z (last point)]
(println "x:" x ", y:" y ", z:" z)))
데이터 구조를 분해하기 위해 first, second, nth, get을 쓰는 코드를 보면, 거의 항상 구조 분해로 정리할 수 있어요. 먼저 let부터 다시 써 볼게요.
(defn print-coordinates-2 [point]
(let [[x y z] point]
(println "x:" x ", y:" y ", z:" z)))
Clojure에서 함수를 정의할 때는 let처럼 들어오는 파라미터에 구조 분해를 적용할 수 있어요.
(defn print-coordinates-3 [[x y z]]
(println "x:" x ", y:" y ", z:" z))
들어오는 point 데이터를 분해하던 여러 줄의 코드를, 그 데이터의 구조를 간결하게 서술하면서 동시에 로컬 값에 바인딩하는 한 문장으로 바꿨어요.
좀 더 현실적인 예시로, 그 유명한 John Smith의 기본 연락처 정보를 담은 맵을 만들어 볼게요.
(def john-smith {:f-name "John"
:l-name "Smith"
:phone "555-555-5555"
:company "Functional Industries"
:title "Sith Lord of Git"})
이제 John의 개인 정보가 있으니 이 맵 안의 값들에 접근해야 해요.
(defn print-contact-info [{:keys [f-name l-name phone company title]}]
(println f-name l-name "is the" title "at" company)
(println "You can reach him at" phone))
(print-contact-info john-smith)
;= John Smith is the Sith Lord of Git at Functional Industries
;= You can reach him at 555-555-5555
이 함수는 :keys 축약으로 입력을 연관 구조 분해하고 나서 우리가 준 연락처 정보를 출력해요.
그럼 John에게 편지를 보내려면 어떨까요?
(def john-smith {:f-name "John"
:l-name "Smith"
:phone "555-555-5555"
:address {:street "452 Lisp Ln."
:city "Macroville"
:state "Kentucky"
:zip "81321"}
:hobbies ["running" "hiking" "basketball"]
:company "Functional Industries"
:title "Sith Lord of Git"})
이제 주소가 들어 있네요. 다만 이걸 하려면 원래 구조 안에 맵을 중첩해야 했어요.
(defn print-contact-info
[{:keys [f-name l-name phone company title]
{:keys [street city state zip]} :address
[fav-hobby second-hobby] :hobbies}]
(println f-name l-name "is the" title "at" company)
(println "You can reach him at" phone)
(println "He lives at" street city state zip)
(println "Maybe you can write to him about" fav-hobby "or" second-hobby))
(print-contact-info john-smith)
;= John Smith is the Sith Lord of Git at Functional Industries
;= You can reach him at 555-555-5555
;= He lives at 452 Lisp Ln. Macroville Kentucky 81321
;= Maybe you can write to him about running or hiking
매크로(Macros)
매크로를 작성하다 보면 구조 분해를 포함하는 매크로를 만들어야 할 때가 있어요. 가장 흔한 방법은 이미 구조 분해를 하는 무언가(let, loop, fn 등)를 호출하는 코드를 만들어 내는 거예요. clojure.core에도 if-let, when-let, when-some 같은 예시가 있죠.
하지만 드물게 매크로 안에서 직접 구조 분해를 처리하고 싶을 때도 있어요. 이럴 때는 (문서화되지 않은) clojure.core/destructure 함수를 쓰면 돼요. 이 함수가 구조 분해 로직을 구현한 것이고, let과 loop가 실제로 호출하는 함수이기도 해요. destructure 함수는 매크로 안에서 호출하도록 설계됐고, 폼을 받아 폼을 돌려주도록 기대돼요.
(destructure '[[x & remaining :as all] numbers])
;= [vec__1 numbers
;= x (clojure.core/nth vec__1 0 nil)
;= remaining (clojure.core/nthnext vec__1 1)
;= all vec__1]
결과는 더 명확하게 보려고 포맷한 거예요. 이 예시는 또 구조 분해가 내부적으로 어떻게 동작하는지에 대한 통찰도 줘요.