Зависит от Хаскеля, сейчас?
Haskell, в небольшой степени, является типизированным языком. Существует понятие данных уровня типа, теперь более разумно типизированное благодаря этому DataKinds
, и есть некоторые средства ( GADTs
), чтобы дать представление во время выполнения для данных уровня типа. Следовательно, значения содержимого во время выполнения эффективно отображаются в типах , что означает, что язык должен быть типизирован зависимым образом.
Простые типы данных повышаются до уровня вида, так что содержащиеся в них значения могут использоваться в типах. Отсюда и архетипический пример
data Nat = Z | S Nat
data Vec :: Nat -> * -> * where
VNil :: Vec Z x
VCons :: x -> Vec n x -> Vec (S n) x
становится возможным, и с этим, такие определения, как
vApply :: Vec n (s -> t) -> Vec n s -> Vec n t
vApply VNil VNil = VNil
vApply (VCons f fs) (VCons s ss) = VCons (f s) (vApply fs ss)
что приятно Обратите внимание, что длина n
является чисто статической функцией в этой функции, гарантируя, что входной и выходной векторы имеют одинаковую длину, даже если эта длина не играет роли в выполнении
vApply
. Напротив, это гораздо сложнее (т.е. невозможно) реализовать функцию , которая делает n
копии данность x
(что было бы , pure
чтобы vApply
«s <*>
)
vReplicate :: x -> Vec n x
потому что очень важно знать, сколько копий нужно сделать во время выполнения. Введите синглтоны.
data Natty :: Nat -> * where
Zy :: Natty Z
Sy :: Natty n -> Natty (S n)
Для любого продвигаемого типа мы можем построить одноэлементное семейство, индексированное по продвигаемому типу, в котором присутствуют дубликаты его значений во время выполнения. Natty n
тип времени выполнения копий уровня типа n
:: Nat
. Теперь мы можем написать
vReplicate :: Natty n -> x -> Vec n x
vReplicate Zy x = VNil
vReplicate (Sy n) x = VCons x (vReplicate n x)
Таким образом, у вас есть значение уровня типа, привязанное к значению времени выполнения: проверка копии во время выполнения уточняет статическое знание значения уровня типа. Даже если термины и типы разделены, мы можем работать зависимым образом, используя одноэлементную конструкцию в качестве эпоксидной смолы, создавая связи между фазами. Это долгий путь от разрешения произвольных выражений во время выполнения в типах, но это не что иное.
Что противного? Чего не хватает?
Давайте немного надавим на эту технологию и посмотрим, что начинает колебаться. Мы могли бы прийти к мысли, что синглтоны должны быть управляемыми немного более неявно
class Nattily (n :: Nat) where
natty :: Natty n
instance Nattily Z where
natty = Zy
instance Nattily n => Nattily (S n) where
natty = Sy natty
позволяя нам писать, скажем,
instance Nattily n => Applicative (Vec n) where
pure = vReplicate natty
(<*>) = vApply
Это работает, но теперь это означает, что наш оригинальный Nat
тип породил три копии: вид, одноэлементное семейство и одноэлементный класс. У нас довольно неуклюжий процесс обмена явными Natty n
значениями и Nattily n
словарями. Более того, Natty
это не так Nat
: у нас есть какая-то зависимость от значений времени выполнения, но не от того типа, о котором мы сначала думали. Никакой полностью типизированный язык не делает зависимые типы такими сложными!
Между тем, хотя Nat
может быть продвинут, Vec
не может. Вы не можете индексировать по индексируемому типу. Полный язык с зависимой типизацией не налагает таких ограничений, и в моей карьере в качестве демонстрации с зависимой типизацией я научился включать примеры двухслойной индексации в свои выступления, просто чтобы научить людей, которые сделали однослойную индексацию трудно, но возможно, не ожидать, что я свернусь, как карточный домик. В чем проблема? Равенство. GADT работают путем преобразования неявных ограничений, которые вы получаете, когда вы предоставляете конструктору конкретный тип возвращаемого значения в явные эквалайзерные требования. Как это.
data Vec (n :: Nat) (x :: *)
= n ~ Z => VNil
| forall m. n ~ S m => VCons x (Vec m x)
В каждом из наших двух уравнений обе стороны имеют вид Nat
.
Теперь попробуйте тот же перевод для чего-то индексированного по векторам.
data InVec :: x -> Vec n x -> * where
Here :: InVec z (VCons z zs)
After :: InVec z ys -> InVec z (VCons y ys)
становится
data InVec (a :: x) (as :: Vec n x)
= forall m z (zs :: Vec x m). (n ~ S m, as ~ VCons z zs) => Here
| forall m y z (ys :: Vec x m). (n ~ S m, as ~ VCons y ys) => After (InVec z ys)
и теперь мы формируем эквациональные ограничения между as :: Vec n x
и
VCons z zs :: Vec (S m) x
там, где две стороны имеют синтаксически различные (но доказуемо равные) виды. Ядро GHC в настоящее время не оборудовано для такой концепции!
Что еще не хватает? Ну, большая часть Haskell отсутствует на уровне типов. Язык терминов, который вы можете продвигать, имеет только переменные и не-GADT конструкторы. Когда у вас есть такие type family
механизмы, вы можете писать программы на уровне типов: некоторые из них могут быть очень похожи на функции, которые вы бы сочли нужными для написания на уровне терминов (например, оснащение Nat
сложением, так что вы можете дать хороший тип для добавления Vec
) , но это просто совпадение!
Еще одна вещь, отсутствующая на практике, - это библиотека, которая использует наши новые возможности для индексации типов по значениям. Что делать Functor
и Monad
стать в этом дивном новом мире? Я думаю об этом, но многое еще предстоит сделать.
Запуск программ уровня типа
Haskell, как и большинство языков программирования с независимой типизацией, имеет две
операционные семантики. Есть способ, которым система времени выполнения запускает программы (только закрытые выражения, после стирания типов, высоко оптимизируется), а затем есть способ, которым средство проверки типов запускает программы (ваши семейства типов, ваш "тип класса Prolog" с открытыми выражениями). Для Haskell вы обычно не смешиваете их, потому что выполняемые программы на разных языках. Языки с независимой типизацией имеют отдельные модели времени выполнения и статические модели выполнения для одного и того же языка программ, но не волнуйтесь, модель времени выполнения все еще позволяет вам стирать типы и, действительно, проверять удаление : это то, что извлекает Coqмеханизм дает вам; это по крайней мере то, что делает компилятор Эдвина Брэди (хотя Эдвин стирает излишне дублированные значения, а также типы и доказательства). Фазовое различие, возможно, уже не является различием синтаксической категории
, но оно живое и здоровое.
Языки с независимой типизацией, будучи тотальными, позволяют типографу запускать программы без страха перед чем-либо худшим, чем долгое ожидание. Поскольку Haskell становится более зависимым типом, мы сталкиваемся с вопросом о том, какой должна быть его статическая модель выполнения? Один из подходов может заключаться в том, чтобы ограничить статическое выполнение всеми функциями, что даст нам такую же свободу выполнения, но может заставить нас проводить различия (по крайней мере, для кода уровня типа) между данными и кодатами, чтобы мы могли сказать, следует ли обеспечить прекращение или производительность. Но это не единственный подход. Мы свободны в выборе гораздо более слабой модели выполнения, которая не желает запускать программы, за счет того, что из-за вычислений получается меньше уравнений. По сути, именно этим и занимается GHC. Правила набора для ядра GHC не упоминают о запуске
программы, но только для проверки доказательств для уравнений. При переводе в ядро средство решения ограничений GHC пытается запустить ваши программы уровня типов, генерируя небольшой серебристый след, свидетельствующий о том, что данное выражение равно его нормальной форме. Этот метод генерации доказательств немного непредсказуем и неизбежно неполон: он борется со страшно выглядящей рекурсией, например, и это, вероятно, разумно. Одна вещь, о которой нам не нужно беспокоиться, это выполнение IO
вычислений в проверщике типов: помните, что проверщик типов не должен давать
launchMissiles
того же значения, что и система времени выполнения!
Культура Хиндли-Милнера
Система типов Хиндли-Милнера достигает поистине удивительного совпадения четырех различных различий с неблагоприятным культурным побочным эффектом, который многие люди не могут увидеть различие между различиями и считают, что совпадение неизбежно! О чем я говорю?
- условия против типов
- явно написанные вещи против неявно написанных вещей
- присутствие во время выполнения против стирания до выполнения
- независимая абстракция против зависимой количественной оценки
Мы привыкли писать термины и оставлять типы, которые будут выведены ... и затем стерты. Мы привыкли количественно определять переменные типа с соответствующей абстракцией типа, и приложение происходит тихо и статично.
Вам не нужно слишком далеко отклоняться от ванильного Хиндли-Милнера, пока эти различия не выровнялись, и это неплохо . Для начала, у нас могут быть более интересные типы, если мы хотим написать их в нескольких местах. Между тем нам не нужно писать словари классов типов, когда мы используем перегруженные функции, но эти словари обязательно присутствуют (или встроены) во время выполнения. В языках с зависимой типизацией мы ожидаем стереть не только типы во время выполнения, но (как и с классами типов), что некоторые неявно выведенные значения не будут стерты. Например, vReplicate
числовой аргумент часто выводится из типа требуемого вектора, но нам все равно нужно знать его во время выполнения.
Какие языковые варианты дизайна мы должны рассмотреть, потому что эти совпадения больше не имеют места? Например, правильно ли, что на Haskell нет способа forall x. t
явно создать экземпляр квантора? Если проверяющий не может угадать, x
используя unifi t
, у нас нет другого способа сказать, что x
должно быть.
В более широком смысле, мы не можем трактовать «вывод типа» как монолитное понятие, которое у нас есть либо полностью, либо ничего. Для начала нам нужно отделить аспект «обобщения» (правило «разрешить» Милнера), который в значительной степени опирается на ограничение того, какие типы существуют, чтобы гарантировать, что глупая машина может угадать один, из аспекта «специализации» (переменная Милнера) «Правило), который так же эффективен, как ваш решатель ограничений. Мы можем ожидать, что типы верхнего уровня станут труднее вывести, но информацию о внутренних типах будет довольно легко распространять.
Следующие шаги для Haskell
Мы видим, что уровни типов и типов очень похожи (и они уже имеют внутреннее представление в GHC). Мы могли бы также слить их. Было бы интересно взять, * :: *
если мы можем: мы потеряли
логическую обоснованность давно, когда мы допустили дно, но
правильность типа обычно является более слабым требованием. Мы должны проверить. Если мы должны иметь разные уровни типов, типов и т. Д., Мы можем по крайней мере убедиться, что все на уровне типов и выше всегда можно продвигать. Было бы замечательно просто повторно использовать полиморфизм, который у нас уже есть для типов, а не заново изобретать полиморфизм на уровне вида.
Мы должны упростить и обобщить существующую систему ограничений, допуская гетерогенные уравнения, a ~ b
где виды a
и
b
синтаксически не идентичны (но могут быть доказаны равными). Это старая техника (в моем тезисе, в прошлом веке), которая значительно облегчает зависимость. Мы могли бы выразить ограничения на выражения в GADT и таким образом ослабить ограничения на то, что можно продвигать.
Мы должны устранить необходимость в одноэлементной конструкции, введя зависимый тип функции pi x :: s -> t
. Функция с таким типом может быть применена в явном виде к любому выражению типа, s
которое находится на пересечении языков типов и терминов (так что переменные, конструкторы, а другие будут описаны позже). Соответствующая лямбда и приложение не будут удалены во время выполнения, поэтому мы сможем написать
vReplicate :: pi n :: Nat -> x -> Vec n x
vReplicate Z x = VNil
vReplicate (S n) x = VCons x (vReplicate n x)
без замены Nat
на Natty
. Домен pi
может быть любого типа, способного к продвижению, поэтому, если GADT можно продвигать, мы можем написать зависимые последовательности кванторов (или «телескопы», как их назвал де Брюйн).
pi n :: Nat -> pi xs :: Vec n x -> ...
на любую длину нам нужно.
Цель этих шагов - устранить сложность , работая напрямую с более общими инструментами, вместо того, чтобы обходиться слабыми инструментами и неуклюжим кодированием. Текущий частичный бай-ин делает преимущества зависимых типов Haskell более дорогими, чем они должны быть.
Слишком сложно?
Зависимые типы заставляют многих нервничать. Они заставляют меня нервничать, но мне нравится нервничать, или, по крайней мере, мне все равно трудно не нервничать. Но это не помогает, что вокруг этой темы существует такой туман невежества. Отчасти это связано с тем, что нам всем еще есть чему поучиться. Но сторонники менее радикальных подходов, как известно, разжигают страх перед зависимыми типами, не всегда следя за тем, чтобы факты были полностью с ними. Я не буду называть имен. Эти «неразрешимые проверки типов», «неполная проверка Тьюринга», «отсутствие различий фаз», «отсутствие стирания типов», «доказательства повсюду» и т. Д. Мифы сохраняются, даже если они являются мусором.
Это, конечно, не тот случай, когда программы с зависимой типизацией всегда должны быть проверены. Можно улучшить базовую гигиену своих программ, применяя дополнительные инварианты в типах, не переходя к полной спецификации. Небольшие шаги в этом направлении нередко приводят к гораздо более сильным гарантиям с небольшим количеством дополнительных доказательств или без таковых. Это неправда, что программы с зависимой типизацией неизбежно полны доказательств, в действительности, я обычно принимаю наличие любых доказательств в своем коде как сигнал к сомнению в моих определениях .
Ибо, как и при любом увеличении артикуляции, мы становимся свободными говорить как новые, так и нечестные вещи. Например, есть много грязных способов определения бинарных деревьев поиска, но это не значит, что нет хорошего способа . Важно не предполагать, что плохой опыт не может быть улучшен, даже если это мешает эго признать это. Разработка зависимых определений - это новый навык, который требует обучения, и, будучи программистом на Haskell, вы не станете экспертом автоматически! И даже если некоторые программы являются грязными, почему вы отказываете другим в свободе быть справедливым?
Зачем еще беспокоиться о Haskell?
Мне действительно нравятся зависимые типы, но большинство моих хакерских проектов все еще в Хаскеле. Зачем? У Haskell есть классы типов. У Haskell есть полезные библиотеки. У Haskell есть работоспособное (хотя и далеко не идеальное) лечение программирования с эффектами. У Haskell есть промышленный компилятор силы. Языки с зависимой типизацией находятся на гораздо более ранней стадии развития сообщества и инфраструктуры, но мы добьемся этого с реальным изменением поколений в том, что возможно, например, с помощью метапрограммирования и обобщений типов данных. Но вам просто нужно посмотреть на то, что люди делают в результате шагов Хаскелла к зависимым типам, чтобы увидеть, что можно получить большую выгоду, если продвинуть вперед и нынешнее поколение языков.