Я видел, как несколько источников отзываются о том, что "Haskell постепенно становится языком, зависящим от языка". По-видимому, подразумевается, что с увеличением количества языковых расширений Haskell дрейфует в этом общем направлении, но пока не существует.
В основном есть две вещи, которые я хотел бы знать. Во-первых, просто, что означает "быть навязанным языком языком"? (Надеюсь, не слишком технично об этом.)
Второй вопрос: какой недостаток? Я имею в виду, люди знают, что мы движемся таким образом, поэтому для этого должно быть какое-то преимущество. И все же, мы еще не там, так что, должно быть, есть некоторые препятствия, которые останавливают людей на всем пути. У меня создается впечатление, что проблема заключается в резком увеличении сложности. Но, не совсем понимая, что такое зависимость, я не знаю точно.
Я знаю, что каждый раз, когда я начинаю читать о заговоренном языке программирования, текст совершенно непонятен... Предположительно, эта проблема. (?)
Ответ 2
Зависит от Typed Haskell, теперь?
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
<*>
)
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, как и большинство зависимых языков программирования, имеет два
оперативные семантики. Там, где работает система времени выполнения
программы (только закрытые выражения, после стирания типа, очень
оптимизирован), а затем там, где программа typechecker запускает программы
(ваши типы семейств, ваш "класс типа Prolog", с открытыми выражениями). Для Haskell вы обычно не смешиваете
два из них, потому что выполняемые программы находятся в разных
языки. Зависимые типы языков имеют раздельное время выполнения и
статические модели выполнения для одного и того же языка программ, но не
беспокойство, модель времени выполнения по-прежнему позволяет вам стирать тип и, действительно,
доказательство стирания: то, что дает механизм извлечения Coq;
что по крайней мере то, что делает компилятор Эдвина Брэди (хотя Эдвин
стирает излишне дублированные значения, а также типы и
доказательства). Различие в фазе не может быть различием синтаксической категории
дольше, но он жив и здоров.
Зависимые типизированные языки, являющиеся общими, позволяют запускать программу typechecker
программ, свободных от страха перед чем-то хуже, чем долгое ожидание. В виде
Haskell становится более навязчивым, мы сталкиваемся с вопросом о том, что
его статическая модель исполнения должна быть? Один из подходов может заключаться в
ограничить статичное выполнение до полных функций, что позволило бы нам
та же свобода для бега, но может заставить нас сделать различия (по крайней мере
для кода типа) между данными и кодами, чтобы мы могли сказать
будь то принудительное прекращение или производительность. Но это не единственное
подход. Мы можем выбрать гораздо более слабую модель исполнения, которая
неохотно запускать программы, ценой принятия меньших уравнений
просто путем вычисления. И по сути, то, что GHC на самом деле
делает. Правила набора текста для ядра GHC не упоминают о запуске
программ, но только для проверки доказательств для уравнений. когда
перевод на ядро, решатель ограничений GHC пытается запустить ваши программы на уровне уровня,
создавая немного серебристого следа доказательств того, что данное выражение
равна его нормальной форме. Этот метод генерации доказательств немного
непредсказуемым и неизбежно незавершенным: он борется с
страшная рекурсия, например, и, вероятно, мудрая. Один
вещь, о которой нам не нужно беспокоиться, - это выполнение IO
вычислений в typechecker: помните, что typechecker не должен давать
launchMissiles
то же значение, что и система времени выполнения!
Культура Хиндли-Милнера
Система типа Hindley-Milner достигает действительно удивительного совпадения
четырех различных различий, с неудачной культурной
побочный эффект, который многие люди не могут видеть различие между
различия и предположить, что совпадение неизбежно! Что я
говорить о?
- термины против типов
- явно написанные вещи против неявно написанных вещей
- наличие во время выполнения перед стиранием до выполнения
- независимая абстракция и зависимая квантификация
Мы привыкли писать термины и оставлять типы для вывода... и
затем стирается. Мы используем для количественной оценки переменных типа с
соответствующая абстракция типа и применение происходит тихо и
статически.
Вам не нужно слишком далеко отклоняться от ванили Хиндли-Милнера
прежде чем эти различия выйдут из равновесия, и это не плохо. Для начала у нас могут быть более интересные типы, если мы хотим записать их в нескольких
мест. Между тем, нам не нужно писать словари типа класса, когда
мы используем перегруженные функции, но эти словари, безусловно,
присутствует (или встроено) во время выполнения. В зависимости от типизированных языков мы
ожидать стирать больше, чем просто типы во время выполнения, но (как с типом
классы), что некоторые неявно выведенные значения не будут
стерта. Например, vReplicate
числовой аргумент часто выводится из типа нужного вектора, но нам все еще нужно знать его во время выполнения.
Какие варианты выбора языка мы должны рассмотреть, поскольку эти
совпадения уже не выполняются? Например, правильно ли Haskell предоставляет
невозможно каким-либо образом создать экземпляр квантора forall x. t
? Если
typechecker не может угадать x
путем unifiying t
, у нас нет другого способа
скажем, что x
должно быть.
В более широком смысле мы не можем рассматривать "вывод типа" как монолитную концепцию
что у нас есть все или ничего. Для начала нам нужно разделить
с точки зрения "обобщения" (Milner "let" ), которая в значительной степени зависит от
ограничивая, какие типы существуют, чтобы гарантировать, что глупая машина может
угадайте один из аспекта "специализации" (правило "var" Милнера)
который так же эффективен, как и ваш решатель ограничений. Мы можем ожидать, что
типы верхнего уровня станут более сложными, но внутренний тип
информация будет довольно легко распространяться.
Следующие шаги для 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 обладает промышленной прочностью
компилятор. Зависимые типизированные языки находятся на гораздо более ранней стадии
в растущем сообществе и инфраструктуре, но мы доберемся туда, с
реальный смена поколений в том, что возможно, например, посредством
метапрограммирование и общие типы данных. Но вам просто нужно смотреть
вокруг того, что люди делают в результате шагов Хаскелла в направлении
чтобы увидеть, что существует большая польза от
также надавливая вперед поколение языков вперед.