Щоб розширити відповідь @ 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)
Довжина вихідного вектора була б сумою довжин двох вхідних векторів. Нам потрібно навчити контролеру типу додавати Nat
s разом. Для цього ми використовуємо функцію рівня типу :
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
визначати, чи рівні два Natty
s.
Щоб перевірити рівність двох 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
пари AVec
s невідомої довжини.
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