Предположим, я хочу написать библиотеку, которая работает с векторами и матрицами. Можно ли объединить измерения в типы, чтобы операции несовместимых измерений вызывали ошибку во время компиляции?
Например, я хотел бы, чтобы подпись точечного продукта была чем-то вроде
dotprod :: Num a, VecDim d => Vector a d -> Vector a d -> a
где d
тип содержит одно целочисленное значение (представляющее размерность этих векторов).
Я полагаю, что это можно сделать путем определения (вручную) отдельного типа для каждого целого числа и сгруппировать их в класс типов с именем VecDim
. Есть ли какой-то механизм для «генерации» таких типов?
Или, возможно, какой-то лучший / более простой способ достижения того же самого?
haskell
type-systems
type-safety
mitchus
источник
источник
tensor
библиотека достигает этого довольно элегантно, используя рекурсивноеdata
определение: noaxiom.org/tensor-documentation#ordinalsОтветы:
Чтобы расширить ответ @ KarlBielefeldt, вот полный пример того, как реализовать Векторы - списки со статически известным числом элементов - в Haskell. Держись за свою шляпу ...
Как видно из длинного списка
LANGUAGE
директив, это будет работать только с последней версией GHC.Нам нужен способ представления длин в системе типов. По определению, натуральное число - это либо ноль (
Z
), либо преемник другого натурального числа (S n
). Так, например, номер 3 будет написаноS (S (S Z))
.С расширением DataKinds , эта
data
декларация представляет вид называетсяNat
и два типа конструкторы называютS
иZ
- другими словами , мы имеем тип уровня натуральных чисел. Обратите внимание, что типыS
иZ
не имеют каких-либо значений членов - только типы*
имеют значения.Теперь мы вводим GADT, представляющий векторы с известной длиной. Обратите внимание на сигнатуру типа:
Vec
требует, чтобы тип видаNat
(то есть aZ
илиS
тип) представлял его длину.Определение векторов аналогично определению связанных списков, с некоторой дополнительной информацией на уровне типов о его длине. Вектор - это либо
VNil
, в этом случае он имеет длинуZ
(эро), либо этоVCons
ячейка, добавляющая элемент в другой вектор, и в этом случае его длина на один больше, чем у другого вектора (S n
). Обратите внимание, что нет аргумента конструктора типаn
. Он просто используется во время компиляции для отслеживания длины и будет удален до того, как компилятор сгенерирует машинный код.Мы определили тип вектора, который несет статическое знание его длины. Давайте запросим тип нескольких
Vec
s, чтобы понять, как они работают:Точечный продукт работает так же, как и для списка:
vap
, который 'zippily' применяет вектор функций к вектору аргументов, являетсяVec
аппликативным<*>
; Я не поместил это вApplicative
экземпляр, потому что это становится грязным . Также обратите внимание, что я используюfoldr
экземпляр, сгенерированный компиляторомFoldable
.Давайте попробуем это:
Большой! Вы получаете ошибку времени компиляции, когда пытаетесь использовать
dot
векторы, длина которых не совпадает.Вот попытка функции объединить векторы:
Длина выходного вектора будет суммой длин двух входных векторов. Нам нужно научить проверку типов, как добавлять
Nat
s вместе. Для этого мы используем функцию уровня типа :В этом
type family
объявлении вводится функция для вызываемых типов:+:
- другими словами, это средство проверки типов для вычисления суммы двух натуральных чисел. Он определен рекурсивно - всякий раз, когда левый операнд больше, чемZ
эро, мы добавляем один к выводу и уменьшаем его на единицу при рекурсивном вызове. (Это хорошее упражнение - написать функцию типа, которая умножает дваNat
с.) Теперь мы можем сделать+++
компиляцию:Вот как вы используете это:
Пока все просто. Как насчет того, когда мы хотим сделать противоположное объединению и разделить вектор на две части? Длины выходных векторов зависят от значения времени выполнения аргументов. Мы хотели бы написать что-то вроде этого:
но, к сожалению, Хаскелл не позволит нам сделать это. Допущение значения этого
n
аргумента появляться в типе возврата (это обычно называется зависимой функция или типа пи ) требует «полного спектра» зависимых типов, в то время какDataKinds
только дает нам способствовали конструкторам типов. Говоря другими словами, тип конструкторовS
иZ
не появляются на ценностном уровне. Нам придется согласиться на одноэлементные значения для представления определенного во время выполненияNat
. *Для данного типа
n
(с видомNat
) существует только один член типаNatty n
. Мы можем использовать одноэлементное значение в качестве свидетеля во время выполненияn
: узнавая о нем,Natty
мы узнаем о немn
и наоборот.Давайте возьмем это для вращения:
В первом примере мы успешно разбили трехэлементный вектор в позиции 2; затем мы получили ошибку типа, когда мы попытались разделить вектор в позиции после конца. Синглтоны - это стандартная техника для зависимости типа от значения в Haskell.
*
singletons
Библиотека содержит несколько помощников Template Haskell для генерации одноэлементных значений, какNatty
для вас.Последний пример Как насчет того, когда вы не знаете размерности вашего вектора статически? Например, что если мы пытаемся построить вектор из данных времени выполнения в виде списка? Вам необходимо, чтобы тип вектора зависел от длины входного списка. Другими словами, мы не можем использовать
foldr VCons 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
), который может помочь эмулировать полиморфизм подтипа.Зависимые пары полезны всякий раз, когда статические свойства данных зависят от динамической информации, недоступной во время компиляции. Вот
filter
для векторов:До
dot
двухAVec
с нам нужно доказать GHC, что их длины равны.Data.Type.Equality
определяет GADT, который может быть создан только тогда, когда его аргументы типа одинаковы:Когда вы выполняете поиск по шаблону
Refl
, GHC это знаетa ~ b
. Есть также несколько функций, которые помогут вам работать с этим типом: мы будем использоватьgcastWith
для преобразования между эквивалентными типами иTestEquality
для определения,Natty
равны ли два s.Чтобы проверить равенство двух
Natty
с, мы будем должны использовать тот факт , что если два числа равны, то их преемники также равны (:~:
это конгруэнтно болееS
):Сопоставление с образцом
Refl
слева позволяет GHC это знатьn ~ m
. С этим знанием это тривиальноS n ~ S m
, поэтому GHC позволяет нам сразу же вернуть новоеRefl
.Теперь мы можем написать экземпляр с
TestEquality
помощью простой рекурсии. Если оба числа равны нулю, они равны. Если у обоих чисел есть предшественники, они равны, если предшественники равны. (Если они не равны, просто вернитесьNothing
.)Теперь мы можем собрать кусочки в
dot
паруAVec
с неизвестной длины.Во-первых, сопоставление с образцом в
AVec
конструкторе, чтобы получить представление во время выполнения длин векторов. Теперь используйте,testEquality
чтобы определить, равны ли эти длины. Если они есть, мы будем иметьJust Refl
;gcastWith
будет использовать это доказательство равенства, чтобы гарантировать, чтоdot u v
оно правильно напечатано, выполняя его неявноеn ~ m
предположение.Обратите внимание, что, поскольку вектор без статической информации о его длине, по сути, является списком, мы фактически повторно реализовали версию списка
dot :: Num a => [a] -> [a] -> Maybe a
. Разница в том, что эта версия реализована в терминах векторовdot
. Вот в чем дело: прежде чем средство проверки типов позволит вам позвонитьdot
, вы должны проверить , имеют ли входные списки одинаковую длину, используяtestEquality
. Я склонен получатьif
неверные заявления, но не в зависимости от типа!Вы не можете избежать использования экзистенциальных оболочек на границах вашей системы, когда вы имеете дело с данными времени выполнения, но вы можете использовать зависимые типы везде в вашей системе и сохранять экзистенциальные оболочки на краях, когда вы выполняете проверку входных данных.
Так как
Nothing
это не очень информативно, вы можете дополнительно уточнить тип,dot'
чтобы вернуть доказательство того, что длины не равны (в форме доказательства того, что их различие не равно 0) в случае сбоя. Это очень похоже на стандартную технику использования HaskellEither String a
для возможного возврата сообщения об ошибке, хотя проверочный термин гораздо полезнее в вычислительном отношении, чем строка!На этом завершается этот обзор о некоторых методах, которые распространены в программировании на Haskell с зависимой типизацией. Программирование с такими типами в Haskell действительно круто, но в то же время очень неудобно. Разбить все ваши зависимые данные на множество представлений, которые означают одно и то же -
Nat
тип,Nat
тип,Natty n
синглтон - действительно довольно обременительно, несмотря на наличие генераторов кода, которые помогут с образцом. В настоящее время также существуют ограничения на то, что можно повысить до уровня типа. Это мучительно, хотя! Разум поражает возможностями - в литературе есть примеры на Haskell строго типизированныхprintf
интерфейсов баз данных, механизмов компоновки пользовательского интерфейса ...Если вам нужно больше читать, есть растущее количество литературы о зависимо типизированном Haskell, опубликованном и на таких сайтах, как Stack Overflow. Хорошей отправной точкой является статья о хасохизме - статья проходит этот самый пример (среди прочего), обсуждая некоторые болезненные детали. В статье Singletons демонстрируется техника одноэлементных значений (таких как
Natty
). Для получения дополнительной информации о зависимой типизации в целом, учебник Agda - хорошее место для начала; Кроме того, Idris - это язык в разработке, который (примерно) разработан так, чтобы быть "Haskell с зависимыми типами".источник
Это называется зависимой типизацией . Как только вы узнаете имя, вы сможете найти больше информации о нем, чем когда-либо надеялись. Есть также интересный язык, похожий на haskell, который называется Idris, который использует их изначально. Его автор сделал несколько действительно хороших презентаций на эту тему, которые вы можете найти на YouTube.
источник
newtype Vec2 a = V2 (a,a)
,newtype Vec3 a = V3 (a,a,a)
и так далее, но это не то, о чем вопрос.)Pi (x : A). B
что является функцией отA
до ,B x
гдеx
аргумент функции. Здесь возвращаемый тип функции зависит от выражения, представленного в качестве аргумента. Однако все это можно стереть, это только время компиляции