Типи даних
До цього моменту, для представлення даних, ми використовували мапи, множини, списки та вектори. У більшості випадків такий підхід є оптимальним, але інколи зʼявляється необхідність визначити власні типи. У цій книзі ми будемо називати такі типи типами «дані» (англ. data types).
Тип "дані" має такі властивості:
- Унікальний тип, іменований чи анонімний, на базі типів батьківської платформи.
- Підтримує можливість вбудованої реалізації протоколів
- Має явно оголошену структуру, що використовує поля або замикання
- Демонструє поведінку, подібну до мап (через записи, див. далі)
Конструкція Deftype
Конструкція найнижчого рівня абстракції для створення власних типів у ClojureScript — це макрос deftype
. Для демонстрації роботи цього макросу визначимо тип під назвою User
:
(deftype User [firstname lastname])
Після оголошення типу можна створити екземпляр типу User
. У наступному прикладі крапка після назви типу User
вказує на виклик конструктора.
(def person (User. "Triss" "Merigold"))
Доступ до полів створеного екземпляра здійснюється через крапку:
(.-firstname person)
;; => "Triss"
Типи, визначені за допомогою макросу deftype
(а також defrecord
, який буде розглянуто далі), створюють обʼєкти, адаптовані до типів батьківської платформи, подібні до класів та повʼязані з поточним простором імен. Для зручності, ClojureScript також визначає функцію-конструктор під назвою ->User
, яку можна імпортувати за допомогою директиви :require
.
Наголосимо на тому, що нам не подобається цей тип функції — ми віддаємо перевагу визначенню власних конструкторів із більш ідіоматичними назвами:
(defn make-user
[firstname lastname]
(User. firstname lastname))
Замість виклику ->User
ми використовуємо саме таку функцію.
Макрос Defrecord
Запис(record) — абстракція дещо вищого рівня, ніж type
; саме макросу defrecord
слід віддавати перевагу для визначення типів у ClojureScript.
Як відомо, у ClojureScript переважно використовуються чисті типи, такі як мапи, але для представлення сутностей наших застосунків найчастіше нам потрібні іменовані типи, і тоді у нагоді стають записи.
Запис (англ. record) — це тип, який реалізує протокол типу «мапа» і, таким чином, може бути застосований, як будь-яка мапа, але завдяки тому, що записи — це власні типи, через протоколи вони реалізують поліморфізм, заснований на типах.
Як наслідок, ми одночасно отримали переваги мап та типів, що можуть виступати у різних абстракціях.
Визначимо тип User
за допомогою запису:
(defrecord User [firstname lastname])
Синтаксис defrecord
дуже схожий на deftype
; насправді, за лаштунками макрос defrecord
використовує deftype
у якості низькорівневого примітивного типу для визначення типів.
Зверніть увагу на те, як доступ до полів власного типу відрізняється від чистих типів:
(def person (User. "Yennefer" "of Vengerberg"))
(:firstname person)
;; => "Yennefer"
(get person :firstname)
;; => "Yennefer"
Як ми згадували раніше, записи — це мапи, тому вони демонструють відповідну поведінку:
(map? person)
;; => true
Також, подібно до мап, записи дозволяють створювати нові поля, які не були визначені від початку:
(def person2 (assoc person :age 92))
(:age person2)
;; => 92
Як бачите, функція assoc
поводиться очікуваним чином та повертає новий екземпляр того ж типу, але з новою парою ключ-значення. Але будьте обережні із функцією dissoc
! Поведінка цієї функції із записами трохи відрізняється від поведінки з мапами: якщо видалене поле було опціональним, dissoc
повертає новий запис, але якщо ви видалите обов'язкове поле, функція поверне порожню мапу.
Інша відмінність від мап полягає у тому, що записи не можна викликати як функції:
(def plain-person {:firstname "Yennefer", :lastname "of Vengerberg"})
(plain-person :firstname)
;; => "Yennefer"
(person :firstname)
;; => person.User does not implement IFn protocol.
Для зручності макрос defrecord
, подібно до deftype
, надає доступ до функції ->User
, а також до додаткової функції-конструктора map->User
. Наша думка з цього приводу така сама, як і щодо конструктора, який створюється макросом deftype
: ми рекомендуємо визначати власні конструктори.
(def cirilla (->User "Cirilla" "Fiona"))
(def yen (map->User {:firstname "Yennefer"
:lastname "of Vengerberg"}))
Реалізація протоколів
Обидва примітиви для визначення власних типів, які ми розглянули, дозволяють вбудовану реалізацію протоколів (див. минулий розділ). Визначимо тип для прикладу:
(defprotocol IUser
"A common abstraction for working with user types."
(full-name [_] "Get the full name of the user."))
Тепер ви можете визначити тип із вбудованою реалізацією певної абстракції, у нашому випадку — IUser
:
(defrecord User [firstname lastname]
IUser
(full-name [_]
(str firstname " " lastname)))
;; Create an instance.
(def user (User. "Yennefer" "of Vengerberg"))
(full-name user)
;; => "Yennefer of Vengerberg"
Макрос Reify
Макрос reify
представляє собою конструктор спеціального призначення (англ. ad hoc constructor), який дозволяє створювати об'єкти без попереднього визначення типу. Реалізації протоколів макросу reify
такі самі, як і для deftype
та defrecord
, але на відміну від них, reify
не має доступних полів.
Ось, як можна емулювати екземпляр типу user
, який добре поєднується із абстракцією IUser
:
(defn user
[firstname lastname]
(reify
IUser
(full-name [_]
(str firstname " " lastname))))
(def yen (user "Yennefer" "of Vengerberg"))
(full-name yen)
;; => "Yennefer of Vengerberg"
Макрос Specify
Макрос specify!
— це покращена альтернатива reify
, яка дозволяє додавати реалізації протоколів до існуючих об'єктів JavaScript. Це може бути корисно, якщо ви хочете додати протоколи до компонентів стандартної бібліотеки JavaScript.
(def obj #js {})
(specify! obj
IUser
(full-name [_]
"my full name"))
(full-name obj)
;; => "my full name"
specify
— це незмінна версія макросу specify!
. specify
можна використовувати із незмінним значеннями, які реалізують протокол ICloneable
(наприклад, колекції ClojureScript).
(def a {})
(def b (specify a
IUser
(full-name [_]
"my full name")))
(full-name a)
;; Error: No protocol method IUser.full-name defined for type cljs.core/PersistentArrayMap: {}
(full-name b)
;; => "my full name"