Щоб розширити відповідь @ KarlBielefeldt, ось повний приклад того, як реалізувати вектори - списки зі статистично відомою кількістю елементів - у Haskell. Тримайся капелюха ...
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE DeriveFoldable #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE DeriveTraversable #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE TypeFamilies #-}
import Prelude hiding (foldr, zipWith)
import qualified Prelude
import Data.Type.Equality
import Data.Foldable
import Data.Traversable
Як видно з довгого списку LANGUAGEдиректив, це працюватиме лише з останньою версією GHC.
Нам потрібен спосіб представлення довжин в системі типів. За визначенням, натуральне число дорівнює нулю ( Z) або є наступником якогось іншого натурального числа ( S n). Так, наприклад, було б написано число 3 S (S (S Z)).
data Nat = Z | S Nat
З розширенням DataKinds , ця dataдекларація являє вид називається Natі два типу конструктори називають Sі Z- іншими словами , ми маємо тип рівня натуральних чисел. Зауважте, що типи Sі Zне мають жодних значень-членів - лише типи роду *населені значеннями.
Тепер ми представляємо GADT, що представляє вектори з відомою довжиною. Зверніть увагу на підпис роду: Vecпотрібен тип родуNat (тобто a Zабо Sтип) для відображення його довжини.
data Vec :: Nat -> * -> * where
VNil :: Vec Z a
VCons :: a -> Vec n a -> Vec (S n) a
deriving instance (Show a) => Show (Vec n a)
deriving instance Functor (Vec n)
deriving instance Foldable (Vec n)
deriving instance Traversable (Vec n)
Визначення векторів схоже на визначення зв'язаних списків з додатковою інформацією на рівні про його довжину. Вектор є або VNilв цьому випадку він має довжину Z(ero), або це VConsклітина, що додає елемент до іншого вектора, і в цьому випадку його довжина на одну більше, ніж інший вектор ( S n). Зауважте, що немає аргументу конструктора типу n. Він просто використовується під час компіляції для відстеження довжин і буде стертий до того, як компілятор генерує машинний код.
Ми визначили тип вектора, який містить статичні знання про його довжину. Давайте запитаємо тип кількох Vecс, щоб відчути, як вони працюють:
ghci> :t (VCons 'a' (VCons 'b' VNil))
(VCons 'a' (VCons 'b' VNil)) :: Vec ('S ('S 'Z)) Char -- (S (S Z)) means 2
ghci> :t (VCons 13 (VCons 11 (VCons 3 VNil)))
(VCons 13 (VCons 11 (VCons 3 VNil))) :: Num a => Vec ('S ('S ('S 'Z))) a -- (S (S (S Z))) means 3
Точковий продукт діє так само, як і для списку:
-- note that the two Vec arguments are declared to have the same length
vap :: Vec n (a -> b) -> Vec n a -> Vec n b
vap VNil VNil = VNil
vap (VCons f fs) (VCons x xs) = VCons (f x) (vap fs xs)
zipWith :: (a -> b -> c) -> Vec n a -> Vec n b -> Vec n c
zipWith f xs ys = fmap f xs `vap` ys
dot :: Num a => Vec n a -> Vec n a -> a
dot xs ys = foldr (+) 0 $ zipWith (*) xs ys
vap, який 'zippily' застосовує вектор функцій до вектора аргументів, є Vecдодатково <*>; Я не ставлю це в Applicativeекземпляр, тому що він стає безладним . Зауважте також, що я використовую foldrекземпляр, створений компілятором Foldable.
Давайте спробуємо:
ghci> let v1 = VCons 2 (VCons 1 VNil)
ghci> let v2 = VCons 4 (VCons 5 VNil)
ghci> v1 `dot` v2
13
ghci> let v3 = VCons 8 (VCons 6 (VCons 1 VNil))
ghci> v1 `dot` v3
<interactive>:20:10:
Couldn't match type ‘'S 'Z’ with ‘'Z’
Expected type: Vec ('S ('S 'Z)) a
Actual type: Vec ('S ('S ('S 'Z))) a
In the second argument of ‘dot’, namely ‘v3’
In the expression: v1 `dot` v3
Чудово! Ви отримуєте помилку часу компіляції при спробі dotвекторів, довжина яких не збігається.
Ось спроба функції об'єднати вектори разом:
-- This won't compile because the type checker can't deduce the length of the returned vector
-- VNil +++ ys = ys
-- (VCons x xs) +++ ys = VCons x (concat xs ys)
Довжина вихідного вектора була б сумою довжин двох вхідних векторів. Нам потрібно навчити контролеру типу додавати Nats разом. Для цього ми використовуємо функцію рівня типу :
type family (n :: Nat) :+: (m :: Nat) :: Nat where
Z :+: m = m
(S n) :+: m = S (n :+: m)
Ця type familyдекларація вводить функцію на типи, що називаються :+:- іншими словами, це рецепт перевірки типу для обчислення суми двох натуральних чисел. Він визначений рекурсивно - коли лівий операнд більший за Zеро, ми додаємо його до виводу та зменшуємо його на одиницю в рекурсивному виклику. (Це гарна вправа, щоб написати функцію типу, яка помножує два Natс.) Тепер ми можемо зробити +++компіляцію:
infixr 5 +++
(+++) :: Vec n a -> Vec m a -> Vec (n :+: m) a
VNil +++ ys = ys
(VCons x xs) +++ ys = VCons x (concat xs ys)
Ось як ви його використовуєте:
ghci> VCons 1 (VCons 2 VNil) +++ VCons 3 (VCons 4 VNil)
VCons 1 (VCons 2 (VCons 3 (VCons 4 VNil)))
Поки так просто. Що робити, коли ми хочемо зробити протилежне конкатенації і розділити вектор на два? Довжина вихідних векторів залежить від значення часу виконання аргументів. Ми хотіли б написати щось подібне:
-- this won't work because there aren't any values of type `S` and `Z`
-- split :: (n :: Nat) -> Vec (n :+: m) a -> (Vec n a, Vec m a)
але, на жаль, Хаскелл не дозволить нам це зробити. Допущення значення цього nаргументу з'являтися в типі повернення (це зазвичай називається залежною функція або типу пі ) вимагає "повного спектра» залежних типів, в той час як DataKindsтільки дає нам сприяли конструкторам типів. Інакше кажучи, тип конструкторів Sі Zне відображається на рівні значень. Нам доведеться погодитись на значення одиночних значень для представлення певного періоду часу Nat. *
data Natty (n :: Nat) where
Zy :: Natty Z -- pronounced 'zed-y'
Sy :: Natty n -> Natty (S n) -- pronounced 'ess-y'
deriving instance Show (Natty n)
Для даного типу n(з родом Nat) існує точно один термін типу Natty n. Ми можемо використовувати одиночне значення в якості свідчення часу для n: навчання про Nattyвчить нас про його nі навпаки.
split :: Natty n ->
Vec (n :+: m) a -> -- the input Vec has to be at least as long as the input Natty
(Vec n a, Vec m a)
split Zy xs = (Nil, xs)
split (Sy n) (Cons x xs) = let (ys, zs) = split n xs
in (Cons x ys, zs)
Давайте візьмемо це за спину:
ghci> split (Sy (Sy Zy)) (VCons 1 (VCons 2 (VCons 3 VNil)))
(VCons 1 (VCons 2 VNil), VCons 3 VNil)
ghci> split (Sy (Sy Zy)) (VCons 3 VNil)
<interactive>:116:21:
Couldn't match type ‘'S ('Z :+: m)’ with ‘'Z’
Expected type: Vec ('S ('S 'Z) :+: m) a
Actual type: Vec ('S 'Z) a
Relevant bindings include
it :: (Vec ('S ('S 'Z)) a, Vec m a) (bound at <interactive>:116:1)
In the second argument of ‘split’, namely ‘(VCons 3 VNil)’
In the expression: split (Sy (Sy Zy)) (VCons 3 VNil)
У першому прикладі ми успішно розділили триелементний вектор у положенні 2; то ми отримали помилку типу, коли ми намагалися розділити вектор у позиції, що минає наприкінці. Синглтонтони - це стандартна методика створення типу, що залежить від значення в Haskell.
* singletonsБібліотека містить кілька помічників шаблону Haskell для генерування однозначних значень, як Nattyдля вас.
Останній приклад. А як бути, коли ти статично не знаєш розмірність вектора? Наприклад, що робити, якщо ми намагаємося побудувати вектор із даних про час виконання у формі списку? Вам потрібен тип вектора залежить від довжини списку введення. Інакше кажучи, ми не можемо використовувати foldr VCons VNilдля побудови вектора, оскільки тип вихідного вектора змінюється з кожною ітерацією складки. Нам потрібно зберегти довжину вектора в таємниці від компілятора.
data AVec a = forall n. AVec (Natty n) (Vec n a)
deriving instance (Show a) => Show (AVec a)
fromList :: [a] -> AVec a
fromList = Prelude.foldr cons nil
where cons x (AVec n xs) = AVec (Sy n) (VCons x xs)
nil = AVec Zy VNil
AVec- це екзистенційний тип : змінна типу nне відображається у типі повернення AVecконструктора даних. Ми використовуємо його для імітації залежної пари : fromListне можемо статично визначити довжину вектора, але він може повернути те, на що ти можеш узгодити шаблон, щоб дізнатися довжину вектора - Natty nу першому елементі кортежу . Як Конор МакБрайд висловлюється у відповідній відповіді , "Ви дивитесь на одне, і роблячи це, дізнаєтесь про інше".
Це звичайна методика для екзистенційно кількісних типів. Оскільки ви фактично нічого не можете зробити з даними, для яких ви не знаєте тип - спробуйте написати функцію data Something = forall a. Sth a- екзистенціали часто поставляються в комплекті з доказами GADT, що дозволяє відновити вихідний тип, виконавши тести на відповідність шаблону. Інші поширені шаблони для екзистенціалів включають функції упаковки для обробки вашого типу ( data AWayToGetTo b = forall a. HeresHow a (a -> b)), що є акуратним способом виконання модулів першого класу, або вбудований словник класу типів ( data AnOrd = forall a. Ord a => AnOrd a), який може допомогти імітувати поліморфізм підтипу.
ghci> fromList [1,2,3]
AVec (Sy (Sy (Sy Zy))) (VCons 1 (VCons 2 (VCons 3 Nil)))
Залежні пари корисні, коли статичні властивості даних залежать від динамічної інформації, недоступної під час компіляції. Ось filterдля векторів:
filter :: (a -> Bool) -> Vec n a -> AVec a
filter f = foldr (\x (AVec n xs) -> if f x
then AVec (Sy n) (VCons x xs)
else AVec n xs) (AVec Zy VNil)
За dotдві AVecсекунди нам потрібно довести GHC, що їх довжини рівні. Data.Type.Equalityвизначає GADT, який може бути побудований лише тоді, коли аргументи його типу однакові:
data (a :: k) :~: (b :: k) where
Refl :: a :~: a -- short for 'reflexivity'
Коли ви узгоджуєте схему Refl, GHC це знає a ~ b. Існує також кілька функцій, які допоможуть вам працювати з цим типом: ми будемо використовувати gcastWithдля перетворення між еквівалентними типами та TestEqualityвизначати, чи рівні два Nattys.
Щоб перевірити рівність двох Nattyз, ми будемо повинні використовувати той факт , що якщо два числа рівні, то їх наступники також рівні ( :~:це конгруентно більш S):
congSuc :: (n :~: m) -> (S n :~: S m)
congSuc Refl = Refl
Збірка шаблону на Reflлівій стороні повідомляє GHC про це n ~ m. Маючи це знання, це неприємно S n ~ S m, тому GHC дозволяє нам повернути нове Reflвідразу.
Тепер ми можемо написати екземпляр TestEqualityпрямої рекурсії. Якщо обидва числа дорівнюють нулю, вони рівні. Якщо обидва числа мають попередників, вони рівні, якщо попередники рівні. (Якщо вони не рівні, просто поверніться Nothing.)
instance TestEquality Natty where
-- testEquality :: Natty n -> Natty m -> Maybe (n :~: m)
testEquality Zy Zy = Just Refl
testEquality (Sy n) (Sy m) = fmap congSuc (testEquality n m) -- check whether the predecessors are equal, then make use of congruence
testEquality Zy _ = Nothing
testEquality _ Zy = Nothing
Тепер ми можемо скласти шматки до dotпари AVecs невідомої довжини.
dot' :: Num a => AVec a -> AVec a -> Maybe a
dot' (AVec n u) (AVec m v) = fmap (\proof -> gcastWith proof (dot u v)) (testEquality n m)
По-перше, узор узгоджується на AVecконструкторі, щоб витягнути уявлення про тривалість векторів. Тепер використовуйте testEqualityдля визначення того, чи рівні ці довжини. Якщо вони є, у нас буде Just Refl; gcastWithвикористовуватиме цей доказ рівності для того, щоб переконатися, що dot u vвін добре набраний, виконуючи його неявне n ~ mприпущення.
ghci> let v1 = fromList [1,2,3]
ghci> let v2 = fromList [4,5,6]
ghci> let v3 = fromList [7,8]
ghci> dot' v1 v2
Just 32
ghci> dot' v1 v3
Nothing -- they weren't the same length
Зауважте, що оскільки вектор без статичних знань про його довжину в основному є списком, ми фактично повторно реалізували версію списку dot :: Num a => [a] -> [a] -> Maybe a. Різниця полягає в тому, що ця версія реалізована з точки зору векторів dot. Ось сенс: перед тим, як перевірка типу дозволить вам телефонувати dot, ви, мабуть, перевірили, чи списки вводу мають однакову довжину testEquality. Я схильний отримувати ifповідомлення невірним способом, але не в залежності від набору!
Ви не можете уникнути використання екзистенціальної обгортки на краях вашої системи, коли ви маєте справу з даними часу виконання, але ви можете використовувати залежні типи скрізь всередині вашої системи та тримати екзистенціальні обгортки по краях під час перевірки вводу.
Оскільки Nothingце не дуже інформативно, ви можете додатково уточнити тип dot'повернення доказів того, що довжини не рівні (у вигляді доказів, що їх різниця не дорівнює 0) у випадку відмови. Це досить схоже на стандартну техніку Haskell, Either String aщоб можливо повернути повідомлення про помилку, хоча термін підтвердження набагато корисніший у порівнянні з рядком!
Таким чином, закінчується ця екскурсія по свистках деяких методів, які є поширеними в програмах Haskell залежно від типу. Програмування подібних типів у Haskell - це справді класно, але при цьому дуже незручно. Розбиття всіх ваших залежних даних на безліч представлень, що означають одне і те ж - Natтип, Natвид, Natty nсинглтон - насправді досить громіздкий, незважаючи на існування генераторів коду, які допоможуть на котлопластіні. В даний час існують обмеження щодо того, що можна підвищити до рівня типу. Це все ж мучить! Розум роздумує над можливостями - в літературі є приклади в Haskell сильно типізованих printf, інтерфейсів бази даних, двигунів компонування інтерфейсу ...
Якщо ви хочете трохи прочитати, є все більша кількість літератури про залежно введені Haskell, як опубліковані, так і на таких сайтах, як Stack Overflow. Хорошим початковим пунктом є папір про хасохізм - ця стаття подає цей самий приклад (серед інших), дещо детально обговорюючи хворобливі частини. Сінглтон папір демонструє техніку значень одноелементні (наприклад, ). Для отримання додаткової інформації про залежне введення тексту загалом, підручник з Agda - це гарне місце для початку; Крім того, Idris - це мова в розробці, яка (орієнтовно) розроблена як "Haskell із залежними типами".Natty