Почему бы не быть навязчивым?

Я видел, как несколько источников отзываются о том, что "Haskell постепенно становится языком, зависящим от языка". По-видимому, подразумевается, что с увеличением количества языковых расширений Haskell дрейфует в этом общем направлении, но пока не существует.

В основном есть две вещи, которые я хотел бы знать. Во-первых, просто, что означает "быть навязанным языком языком"? (Надеюсь, не слишком технично об этом.)

Второй вопрос: какой недостаток? Я имею в виду, люди знают, что мы движемся таким образом, поэтому для этого должно быть какое-то преимущество. И все же, мы еще не там, так что, должно быть, есть некоторые препятствия, которые останавливают людей на всем пути. У меня создается впечатление, что проблема заключается в резком увеличении сложности. Но, не совсем понимая, что такое зависимость, я не знаю точно.

Я знаю, что каждый раз, когда я начинаю читать о заговоренном языке программирования, текст совершенно непонятен... Предположительно, эта проблема. (?)

Ответ 1

Зависимая типизация - это просто унификация уровней значений и типов, поэтому вы можете параметризовать значения типов (уже возможно с типами классов и параметрическим полиморфизмом в Haskell), и вы можете параметризовать типы значений (не, строго говоря, возможно но в Haskell, хотя DataKinds очень близко).

Изменить: По-видимому, с этого момента я ошибся (см. комментарий @pigworker). Я оставлю все это в качестве записи о мифах, которые мне кормили.: P


Проблема с переходом к полной зависимой типизации, из того, что я слышал, заключается в том, что она нарушит ограничение фазы между уровнями и уровнями значений, которые позволят скомпилировать Haskell для эффективного машинного кода со стираемыми типами. В соответствии с нашим современным уровнем технологии, в зависимости от того, какой язык набирается в зависимости от языка, он должен проходить через интерпретатор в какой-то момент (либо сразу, либо после компиляции на байт-код с аналогичным типом или аналогичный).

Это не обязательно фундаментальное ограничение, но я лично не знаю о каких-либо текущих исследованиях, которые выглядят многообещающими в этом отношении, но это еще не дошло до GHC. Если кто-то еще знает больше, я был бы рад, если бы меня исправили.

Ответ 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 обладает промышленной прочностью компилятор. Зависимые типизированные языки находятся на гораздо более ранней стадии в растущем сообществе и инфраструктуре, но мы доберемся туда, с реальный смена поколений в том, что возможно, например, посредством метапрограммирование и общие типы данных. Но вам просто нужно смотреть вокруг того, что люди делают в результате шагов Хаскелла в направлении чтобы увидеть, что существует большая польза от также надавливая вперед поколение языков вперед.

Ответ 3

Джон - это еще одно распространенное заблуждение относительно зависимых типов: они не работают, когда данные доступны только во время выполнения. Вот как вы можете сделать пример getLine:

data Some :: (k -> *) -> * where
  Like :: p x -> Some p

fromInt :: Int -> Some Natty
fromInt 0 = Like Zy
fromInt n = case fromInt (n - 1) of
  Like n -> Like (Sy n)

withZeroes :: (forall n. Vec n Int -> IO a) -> IO a
withZeroes k = do
  Like n <- fmap (fromInt . read) getLine
  k (vReplicate n 0)

*Main> withZeroes print
5
VCons 0 (VCons 0 (VCons 0 (VCons 0 (VCons 0 VNil))))

Изменить: Hm, это должно было быть комментарием к ответчику свиней. Я явно терпеть неудачу в SO.

Ответ 4

Поводчик дает прекрасное обсуждение того, почему мы должны двигаться к зависимым типам: (a) они потрясающие; (б) они фактически упростили бы то, что уже делает Haskell.

Что касается "почему бы и нет"? вопрос, есть несколько моментов, которые я думаю. Первый момент состоит в том, что, хотя основное понятие за зависимыми типами легко (разрешить типы зависимости от значений), разветвления этого основного понятия являются как тонкими, так и глубокими. Например, различия между значениями и типами все еще живы и хороши; но обсуждение различий между ними становится намного более тонким, чем в вашем Hindley - Milner или System F. В некоторой степени это связано с тем, что зависимые типы в основном жесткие (например, логика первого порядка неразрешима). Но я думаю, что большая проблема заключается в том, что нам не хватает хорошего словарного запаса для сбора и объяснения того, что происходит. Поскольку все больше и больше людей узнают о зависимых типах, мы разработаем лучший словарный запас, и поэтому все станет понятнее, даже если основные проблемы все еще трудны.

Второй момент связан с тем, что Haskell растет в сторону зависимых типов. Поскольку мы делаем постепенный прогресс в достижении этой цели, но, фактически не делая этого, мы застряли на языке, который имеет инкрементные исправления поверх инкрементных патчей. То же самое произошло на других языках, так как новые идеи стали популярными. Java не использовал, чтобы иметь (параметрический) полиморфизм; и когда они, наконец, добавили его, это было очевидно постепенное улучшение с некоторыми утечками абстракции и искалеченной властью. Оказывается, смешивание подтипов и полиморфизм по своей сути затруднено; но это не причина, почему Java Generics работают так, как они делают. Они работают так, как они делают из-за ограничения на постепенное улучшение старых версий Java. То же самое, еще в тот день, когда был изобретен ООП, и люди начали писать "объективные" C (не путать с Objective C) и т.д. Помните, что С++ начинался под видом строгого набора C. Добавление новых парадигмы всегда требуют определения языка заново, или в конечном итоге с некоторым сложным беспорядком. Моя точка зрения заключается в том, что добавление истинных зависимых типов к Haskell потребует определенного количества потрохов и реструктуризации языка - если мы будем делать это правильно. Но действительно сложно выполнить такой пересмотр, в то время как постепенный прогресс, который мы делали, в краткосрочной перспективе кажется более дешевым. На самом деле, не так много людей, которые взламывают GHC, но там есть много кода устаревания, чтобы сохранить жизнь. Это часть причины, по которой так много языков spinoff, как DDC, Cayenne, Idris и т.д.