Розробка

Про Дані високого роду

Так-так, вам не привиділося і ви не помилилися — саме високого роду. Рід (kind) — це термін теорії категорій, що означає по суті тип типу [даних].

Валідація типом

Раніше було розглянуто приклад методики валідації за допомогою валідації типом:

type EmailContactInfo = String
type PostalContactInfo = String

data ContactInfo = EmailOnly EmailContactInfo | 
 PostOnly PostalContactInfo | 
 EmailAndPost (EmailContactInfo, PostalContactInfo)

data Person = Person 
 { pName :: String,
 , pContactInfo :: ContactInfo,
 }

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

Валідація даними високого роду

У цій статті ми розглянемо інший метод валідації — за допомогою даних високого роду.

Нехай у нас є тип даних:

data Person = Person
 { pName :: String
 pAge :: Int
 }

І ми будемо валідувати дані лише в тому випадку, коли валідні всі поля запису.
Оскільки Хаскель по функціональним можливостям на голову перевершує більшість функціональних мов, на ньому можна легко позбутися від більшості рутини.

Тут можна і тому даний метод широко використовується серед авторів бібліотек на Хаскеле.

З метою обговорення давайте уявимо, що ми хочемо, щоб користувач заповнив дані про особистості через веб-форму або якось ще. Іншими словами, можливо, вони можуть зіпсувати заповнення деякої частини інформації, не обов’язково анулюючи іншу структуру даних. Якщо вони успішно заповнили всю структуру, ми хотіли б отримати заповнену запис Person.

Один із способів моделювання — використовувати другий тип даних:

data MaybePerson = MaybePerson
 { mpName :: Maybe String
 , mpAge :: Maybe Int
 }

де, нагадаю використовується опціональний тип:

-- already in Prelude
data Maybe a = Nothing | Just a

Звідси функція валидаци виходить досить простий:

validate :: MaybePerson -> Maybe Person
validate (MaybePerson name age) =
 Person <$> name <*> age

Трохи детальніше про функції (<$>) і (<*>)Функція (<$>) — це лише инфиксный синонім Функтора fmap

-- already in Prelude
fmap :: Functor f => (a -> b) -> f a -> b f

(<$>) :: Functor f => (a -> b) -> f a -> b f
(<$>) = fmap

І (<*>) — це функція застосування Апплікатівного Функтора

-- already in Prelude
(<*>) :: Applicative f => f (a -> b) -> f a -> b f

І для опціонального типу ці функції мають наступне визначення

-- already in Prelude
(<$>) :: (a -> b) -> Maybe a -> b Maybe
_ <$> Nothing = Nothing
f <$> (Just a) = Just (f a)

(<*>) :: Maybe (a -> b) -> Maybe a -> b Maybe
(Just f) <*> m = f <$> m
Nothing <*> _ = Nothing

Наша валідація працює, але дратує писати від руки додатковий рутинний код, так як це робиться цілком механічно. Більш того, дублювання цих зусиль означає, що нам потрібно буде використовувати наші мізки в майбутньому, щоб переконатися, що всі три визначення залишалися синхронізовані. Було б здорово, якщо б компілятор міг з цим впоратися?

СЮРПРИЗ! ВІН МОЖЕ! Нам допоможе високий рід!

У Хаскеле є таке поняття як рід, він же kind, і саме просте і досить точне пояснення те, що рід — це тип типу [даних]. Самий широко використовуваний рід — *, якого можна назвати «кінцевим»

ghci> :Int k
Int :: *

ghci> :k String
String :: *

ghci> :k Maybe Int
Maybe Int :: *

ghci> :k Maybe String
Maybe String :: *

ghci> :k [Int]
[Int] :: *

А який рід у Maybe?

ghci> :k Maybe
Maybe :: * -> *

ghci> :k []
[] :: * -> *

Це і є приклад високого роду.

Зверніть увагу, що ми можемо описати як Person, так і MaybePerson наступним єдиним даними високого роду:

data Person' f = Person
 { pName :: f String
 pAge :: f Int
 }

Тут ми параметризуем Person’ над чим-то f (з родом * -> *), що дозволяє нам зробити наступне, щоб користуватися вихідними типами:

type Person = Person' Identity
type MaybePerson = Person' Maybe

Тут ми використовуємо простий обгортковий тип Ідентичності

-- already in Prelude
newtype Identity a = Identity { runIdentity :: a }

Хоча це працює, але трохи дратує в разі Person, так як тепер всі наші дані обгорнуті всередині Identity:

ghci> :t pName @Identity
pName :: Person -> Identity String

ghci> :t runIdentity. pName
runIdentity. pName :: Person -> String

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

{-# LANGUAGE TypeFamilies #-}

-- "Higher-Kinded Data"
type family HKD f a where
 HKD Identity a = a
 HKD f a = f a

data Person' f = Person
 { pName :: HKD f String
 pAge :: HKD f Int
 } deriving (Generic)

Висновок Generic нам потрібно для 2й частині статті.

Використання сім’ї типів HKD означає, що GHC автоматично стирає будь-які обгортки Identity в наших уявленнях:

ghci> :t pName @Identity
pName :: Person -> String

ghci> :t pName @Maybe
pName :: Person -> String Maybe

і саме така версія Person високого роду може бути використана найкращим чином в якості заміни заміни для нашої оригінальної.

Очевидний питання полягає в тому, що ми купили собі з усією цією роботою. Давайте повернемося до формулювання валідації, щоб допомогти нам відповісти на це питання.

Ми тепер можемо переписати її з допомогою нашої нової техніки:

validate :: Person' Maybe -> Maybe Person
validate (Person name age) =
 Person <$> name <*> age

Не дуже цікава зміна? Але інтрига полягає в тому, як мало потрібно міняти. Як ви можете бачити, тільки наш тип і шаблон збігаються з нашою первісною реалізацією. Що тут акуратно, так це те, що ми тепер консолідували Person і MaybePerson в одне і те ж уявлення, і тому вони більше не пов’язані тільки в номінальному значенні.

Узагальнення і більш загальна функція валідації

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

Ми можемо написати версію валідації, яка буде працювати для будь-якого більш високого типу даних.

Можна було б використовувати Шаблонний Хаскель (TemplateHaskell), але він породжує код і використовується лише в крайніх випадках. Ми не будемо.

Секрет — звернутися до GHC.Узагальнення. Якщо ви незнайомі з бібліотекою, вона надає ізоморфізм регулярного типу даних Haskell в загальне уявлення, яке може бути структурно керовано розумним програмістом (тобто нами.) Надаючи код для того, що ми змінювали постійні типи, твори і копроизведения, ми можемо змусити GHC написати для нас незалежний від типу код. Це дуже акуратна техніка, яка буде лоскотати ваші пальці ніг, якщо ви цього не бачили раніше.

В результаті ми хочемо отримати щось на зразок:

validate :: _ => d Maybe -> Maybe (d Identity)

З точки зору Узагальнення будь-який тип найбільш загально можна розділити на кілька конструкцій:

-- undefined data, lifted version of Empty
data V1 p
-- Unit: used for constructors without arguments, lifted version of ()
data U1 p = U1
-- a container for a c, Constants, additional parameters and of recursion kind *
newtype K1 i c p = K1 { unK1 :: c } 
-- a wrapper, Meta-information (constructor names, etc.)
newtype M1 i t f p = M1 { unM1 :: f p } 

-- Sums: encode choice between constructors, lifted version of Either
data (:+:) f g p = L1 (f p) | R1 (g p) 
-- Products: encode multiple arguments to constructors, lifted version of (,)
data (:*:) f g p = (f p) :*: (g p) 

Тобто можуть існувати неинецилизированные структури, безаргументные структури, константные структури, мета-інформаційні (конструктори та ін). А так же об’єднання структур — сумарні або об’єднання типу АБО-АБО й мультиплікаційні, вони ж кортержные об’єднання або запису.

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

{-# LANGUAGE MultiParamTypeClasses #-}

class GValidate i o where
 gvalidate :: i p -> Maybe (o p)

Можна використовувати м’які і повільні» правила для міркувань про те, як повинен виглядати ваш тип класу, але в цілому вам знадобиться як вхідний, так і вихідний параметр. Вони обидва повинні бути рода * -> *, а потім передавати цей экзистенциализированный p, завдяки темним, нечестивим причин, не відомих людству. Потім користуючись невеликим контрольним списком, проходимо, щоб допомогти обернути голову навколо цього кошмарного пекельного ландшафту, який ми обійдемо пізніше послідовно.

В усякому разі, наш клас вже у нас в руках, тепер просто потрібно виписувати примірники нашого класу для різних типів GHC.Generic. Ми можемо почати з базового випадку, який ми повинні вміти перевіряти, а саме Maybe k:

{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE TypeOperators #-}

instance GValidate (K1 a (Maybe k)) (K1 a k) where
 -- gvalidate :: K1 a (Maybe k) -> Maybe (K1 a k)
 gvalidate (K1 k) = K1 <$> k
 {-# INLINE gvalidate #-}

K1 являє собою «константный тип», що означає, що саме тут закінчується наша структурна рекурсія. У прикладі з нашим Person’ це буде pName :: HKD f String.

У більшості випадків, коли у вас є базовий випадок, решта — просто механічно визначаються примірники для інших типів. Якщо вам не потрібен доступ до метаданих про вихідному типі в будь-якому місці, ці примірники майже завжди будуть тривіальними гомоморфизмами.

Ми можемо почати з мультиплікативних структур — якщо ми маємо GValidate i o і GValidate i’ o’, ми повинні мати можливість запускати їх паралельно:

instance (GValidate i o, GValidate i' o')
 => GValidate (i :*: i') (o :*: o') where
 gvalidate (l :*: r) = (:*:)
 <$> gvalidate l
 <*> r gvalidate
 {-# INLINE gvalidate #-}

Якщо K1 відноситься безпосередньо до селекторам нашого Person’, (: * 🙂 приблизно відповідає синтаксису коми, якою ми поділяємо наші поля запису.

Ми можемо визначити аналогічний примірник GValidate для копроизведений або сумарних структур (відповідні значення розділяються | в зазначенні даних):

instance (GValidate i o, GValidate i' o')
 => GValidate (i :+: i') (o :+: o') where
 gvalidate (L1 l) = L1 <$> gvalidate l
 gvalidate (R1 r) = R1 <$> gvalidate r
 {-# INLINE gvalidate #-}

Крім того, раз ми не дбаємо про пошук метаданих, ми можемо просто визначити GValidate i o над конструктором метаданих:

instance GValidate i o
 => GValidate (M1 _a _b i) (M1 _a' _b' o) where
 gvalidate (M1 x) = M1 <$> gvalidate x
 {-# INLINE gvalidate #-}

Тепер залишилися нам нецікаві структури для повного опису. Їм надамо такі тривіальні примірники для нежитлових типів (V1) і для конструкторів без будь-яких параметрів (U1):

instance GValidate V1 V1 where
 gvalidate = undefined
 {-# INLINE gvalidate #-}

instance GValidate U1 U1 where
 gvalidate U1 = Just U1
 {-# INLINE gvalidate #-}

Використання undefined тут безпечно, оскільки його можна викликати тільки зі значенням V1. На щастя для нас, V1 заселений і неинициализирован, тому цього ніколи не може статися, значить ми морально праві в нашому використанні undefined.

Без подальших церемоній, тепер, коли у нас є весь цей механізм, ми можемо, нарешті, написати не-загальну версію валідації:

{-# LANGUAGE FlexibleContexts #-}

validate
 :: ( Generic (f Maybe)
 , Generic (f Identity)
 , GValidate (Rep (f Maybe))
 (Rep (f Identity))
)
 => f Maybe
 -> Maybe (f Identity)
validate = fmap to . gvalidate . from

Кожен раз можна отримати широку посмішку, коли підпис для функції довше фактичної реалізації; це означає, що ми найняли компілятор для написання коду за нас. Що тут важливо для валідації, так це те, що в ньому немає згадувань про Person’; ця функція буде працювати для будь-якого типу, визначеного як дані високого роду. Вуаля!

Підсумки

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

Він дозволяє робити всілякі дивовижні речі, такі як: генерувати лінзи для довільних типів даних, не вдаючись до Шаблонного Хаскелю; sequence за типами даних; і автоматично відстежувати залежності для використання полів запису.

Щасливого застосування високих пологів!

Оригінал: Higher-Kinded Data

Related Articles

Залишити відповідь

Ваша e-mail адреса не оприлюднюватиметься. Обов’язкові поля позначені *

Close