Деструктурування
Деструктурування — це процес вичитування окремих частин з структурованих даних, наприклад колекцій. В ClojureScript є спеціальний синтаксис для деструктурування індексованих послідовностей та асоціативних структур даних, що використовується у локальних зв'язуваннях та в аргументах функцій.
Давайте розглянемо приклад, щоб зрозуміти в яких випадках деструктурування може бути корисним. Звичайно, якщо вам треба прочитати лише перший та третій елемент з колекції, це можна зробити за допомогою функції nth
:
(let [v [0 1 2]
fst (nth v 0)
thrd (nth v 2)]
[thrd fst])
;; => [2 0]
Недоліком такого підходу є його надмірна багатослівність. За допомогою деструктурування це можна зробити у місці створення локальних змінних, використовуючи вектор:
(let [[fst _ thrd] [0 1 2]]
[thrd fst])
;; => [2 0]
У цьому прикладі [fst _ thrd]
— форма деструктурування. Вона представлена у вигляді вектору, що використовується для зв'язування значень в колекції з символами fst
і thrd
відповідно до індексів 0
та 2
цих значень у колекції. Символ _
використовується як заглушка для значень, які нам не потрібні, у цьому випадку це 1
.
Зверніть увагу, що деструктурування не обмежене зв'язуванням у формі let
; воно працює майже усюди, де можливо зв'язати значення з символом, наприклад, у формах for
та doseq
, а також у аргументах функції. За допомогою деструктурування ми можемо створити функцію, що приймає пару значень та міняє їх місцями:
(defn swap-pair [[fst snd]]
[snd fst])
(swap-pair [1 2])
;; => [2 1]
(swap-pair '(3 4))
;; => [4 3]
Деструктурування за позицією в колекції з допомогою вектора добре підходить для вичитування значень з індексованих послідовностей, але інколи ми не хочемо відкидати інші аргументи послідовності. Символ &
використовується в деструктуруванні для групування залишкових аргументів в окрему послідовність так само, як і в аргументах варіаційних функцій:
(let [[fst snd & more] (range 10)]
{:first fst
:snd snd
:rest more})
;; => {:first 0, :snd 1, :rest (2 3 4 5 6 7 8 9)}
У цьому прикладі перше значення в колекції, з індексом 0
, зв'язане з символом fst
, друге, з індексом 1
, — з snd
, а усі інші, починаючи з індексу 2
і до кінця, — з символом more
.
Іноді разом з деструктуруванням нам також може бути потрібна уся структура даних. Це можна зробити за допомогою ключового слова :as
, яке зв'язує початкове значення з символом, що записаний після :as
у формі деструктурування:
(let [[fst snd & more :as original] (range 10)]
{:first fst
:snd snd
:rest more
:original original})
;; => {:first 0, :snd 1, :rest (2 3 4 5 6 7 8 9), :original (0 1 2 3 4 5 6 7 8 9)}
Асоціативні колекції також можна деструктурувати. На відміну від індексованих колекцій, де використовується вектор, така форма деструктурування представлена у вигляді мапи. В цій мапі ключі — це символи, з якими будуть зв'язані значення з оригінальної мапи, а значення — це ключі з оригінальної мапи, значення яких ми хочемо зв'язати з символами у формі деструктурування. Давайте розглянемо наступний приклад:
(let [{language :language} {:language "ClojureScript"}]
language)
;; => "ClojureScript"
У цьому прикладі ми дістаємо значення по ключу :language
і зв'язуємо його з символом language
. Коли у мапі немає значення відповідного до ключа, значенням символу буде nil
:
(let [{name :name} {:language "ClojureScript"}]
name)
;; => nil
В асоціативному деструктуруванні можливо задавати значення за замовчуванням для символів, що не мають значення в мапі по зазначеному ключу. Для цього використовується ключове слово :or
та мапа зі зв'язуваннями по замовчуванню після нього:
(let [{name :name :or {name "Anonymous"}} {:language "ClojureScript"}]
name)
;; => "Anonymous"
(let [{name :name :or {name "Anonymous"}} {:name "Cirilla"}]
name)
;; => "Cirilla"
Асоціативне деструктурування також підтримує зв'язування символу з оригінальним значенням за допомогою ключового слова :as
у формі деструктурування:
(let [{name :name :as person} {:name "Cirilla" :age 49}]
[name person])
;; => ["Cirilla" {:name "Cirilla" :age 49}]
В асоціативних структурах даних ключами можуть бути не лише ключові слова. Числа, рядки, символи та інші структури даних теж можуть бути ключами, тому ми також можемо використовувати їх у деструктуруванні. Якщо ключ — це символ, його треба взяти у лапки, щоб представити як структуру даних, а не змінну, у якої є деяке значення:
(let [{one 1} {0 "zero" 1 "one"}]
one)
;; => "one"
(let [{name "name"} {"name" "Cirilla"}]
name)
;; => "Cirilla"
(let [{lang 'language} {'language "ClojureScript"}]
lang)
;; => "ClojureScript"
Зазвичай значення у деструктуруванні мапи зв'язують з символами з таким же ім'ям, як і ключі у цій мапі (наприклад, значення по ключу :language
до символу language
). Оскільки ключами звичайно бувають ключові слова, рядки або символи, в ClojureScript для цих випадків є скорочений синтаксис деструктурування.
Ми покажемо приклади для всіх типів ключів, починаючи з ключового слова, з використанням :keys
:
(let [{:keys [name surname]} {:name "Cirilla" :surname "Fiona"}]
[name surname])
;; => ["Cirilla" "Fiona"]
Як ви можете бачити, ключове слово :keys
та вектор символів утворюють локальні змінні зі значеннями відповідно до ключів у мапі. Символи у векторі повинні мати таке ж ім'я, як і відповідні ключі. Такий запис є скороченим еквівалентом {name :name surname :surname}
.
Скорочена форма для рядків та символів працює точно так, як і :keys
, але з використанням :strs
та :syms
відповідно:
(let [{:strs [name surname]} {"name" "Cirilla" "surname" "Fiona"}]
[name surname])
;; => ["Cirilla" "Fiona"]
(let [{:syms [name surname]} {'name "Cirilla" 'surname "Fiona"}]
[name surname])
;; => ["Cirilla" "Fiona"]
Цікавою особливістю деструктурування є те, що форми можна вкладувати для вичитування вкладених значень, при цьому це легко читається, оскільки форма деструктурування описує структуру самої колекції:
(let [{[fst snd] :languages} {:languages ["ClojureScript" "Clojure"]}]
[snd fst])
;; => ["Clojure" "ClojureScript"]