Чтобы расширить ответ @ 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
(эро), либо это VCons
ячейка, добавляющая элемент в другой вектор, и в этом случае его длина на один больше, чем у другого вектора ( S n
). Обратите внимание, что нет аргумента конструктора типа n
. Он просто используется во время компиляции для отслеживания длины и будет удален до того, как компилятор сгенерирует машинный код.
Мы определили тип вектора, который несет статическое знание его длины. Давайте запросим тип нескольких Vec
s, чтобы понять, как они работают:
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
Библиотека содержит несколько помощников Template 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-свидетельством, которое позволяет вам восстановить исходный тип, выполнив тесты на соответствие шаблону. Другие распространенные шаблоны для экзистенциалов включают в себя упаковку функций для обработки вашего type ( 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
с неизвестной длины.
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. Хорошей отправной точкой является статья о хасохизме - статья проходит этот самый пример (среди прочего), обсуждая некоторые болезненные детали. В статье Singletons демонстрируется техника одноэлементных значений (таких как Natty
). Для получения дополнительной информации о зависимой типизации в целом, учебник Agda - хорошее место для начала; Кроме того, Idris - это язык в разработке, который (примерно) разработан так, чтобы быть "Haskell с зависимыми типами".