До цього моменту, для представлення даних, ми використовували мапи, множини, списки та вектори. У більшості випадків такий підхід є оптимальним, але інколи зʼявляється необхідність визначити власні типи. У цій книзі ми будемо називати такі типи типами «дані» (англ. data types).
Тип "дані" має такі властивості:
- Унікальний тип, іменований чи анонімний, на базі типів батьківської платформи.
- Підтримує можливість вбудованої реалізації протоколів
- Має явно оголошену структуру, що використовує поля або замикання
- Демонструє поведінку, подібну до мап (через записи, див. далі)
Конструкція найнижчого рівня абстракції для створення власних типів у 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
ми використовуємо саме таку функцію.
Запис(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
представляє собою конструктор спеціального призначення (англ. 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!
— це покращена альтернатива 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"