Перехідні структури даних

Незмінні та стійкі структури даних мови ClojureScript відрізняються доволі високою продуктивністю, але трапляються ситуації, коли необхідно трансформувати великі структури даних за кілька окремих кроків і лише кінцевий результат має бути доступний для інших користувачів. Наприклад, функція стандартної бібліотеки into отримує колекцію та негайно заповнює її змістом наданої послідовності на кожній ітерації:

(into [] (range 100))
;; => [0 1 2 ... 98 99]

В цьому прикладі ми створюємо вектор зі 100 елементів, додаючи щоразу по одному. При цьому вектори, які створюються на проміжних кроках, буде видно лише функції into, а копіювання масиву, що є необхідним для збереження стійкості, стає необовʼязковою витратою.

Для подібних ситуацій у ClojureScript передбачені спеціальні версії певних стійких структур даних, які називаються перехідними (англ. transients). Мапи, вектори та множини мають перехідні аналоги. Перехідні версії завжди створюються на базі відповідних стійких структур за допомогою функції transient, яка формує перехідну версію за постійний час:

(def tv (transient [1 2 3]))
;; => #<[object Object]>

Перехідні версії підтримують інтерфейс читання відповідних стійких аналогів.

(def tv (transient [1 2 3]))

(nth tv 0)
;; => 1

(get tv 2)
;; => 3

(def tm (transient {:language "ClojureScript"}))

(:language tm)
;; => "ClojureScript"

(def ts (transient #{:a :b :c}))

(contains? ts :a)
;; => true

(:a ts)
;; => :a

Щодо запису, через те, що для перехідних структур не визначено семантики стійкості та незмінності, їхні значення не можна оновлювати за допомогою вже знайомих функцій conj та assoc. Для трансформації перехідних структур існують спеціальні функції, назви яких закінчуються знаком оклику. Розглянемо приклад застосування функції conj! до перехідної структури:

(def tv (transient [1 2 3]))

(conj! tv 4)
;; => #<[object Object]>

(nth tv 3)
;; => 4

Як ви можете переконатися, перехідна версія вектора не є ані незмінною, ані стійкою. Натомість, вектор змінюється без створення додаткових структур. Зауважимо, що можна неодноразово змінювати tv за допомогою функції conj!, але не слід відмовлятися від ідіом, які застосовуються до стійких структур: при трансформації перехідної структури для подальших модифікацій використовуйте версію, яка є результатом попередньої трансформації, як у наступному прикладі:

(-> [1 2 3]
  transient
  (conj! 4)
  (conj! 5))
;; => #<[object Object]>

Можна перетворити перехідну структуру на стійку та незмінну шляхом застосування функції persistent!. Як і створення перехідної структури зі стійкої, зворотнє перетворення вимагає постійного часу:

(-> [1 2 3]
  transient
  (conj! 4)
  (conj! 5)
  persistent!)
;; => [1 2 3 4 5]

Особливість перетворення перехідної структури на стійку полягає у тому, що перша після перетворення на другу стає недійсною і її подальші трансформації неможливі. Це пояснюється тим, що похідна стійка структура використовує внутрішні вузли перехідної, а змінення цих вузлів може порушити гарантії стійкості та незмінності:

(def tm (transient {}))
;; => #<[object Object]>

(assoc! tm :foo :bar)
;; => #<[object Object]>

(persistent! tm)
;; => {:foo :bar}

(assoc! tm :baz :frob)
;; Error: assoc! after persistent!

Повернемося до прикладу з функцією into та наведемо дуже спрощену імплементацію такої функції, яка використовує перехідну структуру для підвищення продуктивності, повертає стійку структуру даних і таким чином надає повністю функціональний інтерфейс, але у внутрішніх процесах вдається до мутацій:

(defn my-into
  [to from]
  (persistent! (reduce conj! (transient to) from)))

(my-into [] (range 100))
;; => [0 1 2 ... 98 99]

results matching ""

    No results matching ""