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

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

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

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

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

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

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


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

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

Зависимый Тибетский Хаскелл, теперь?

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

Простые типы данных повышаются до уровня уровня, так что значения, которые они содержат, могут использоваться в типах. Следовательно, архетипический пример

 data Nat = Z | S Nat data Vec :: Nat -> * -> * where VNil :: Vec Z x VCons :: x -> Vec nx -> Vec (S n) x 

становится возможным, а вместе с ним и определения, такие как

 vApply :: Vec n (s -> t) -> Vec ns -> Vec nt vApply VNil VNil = VNil vApply (VCons f fs) (VCons s ss) = VCons (fs) (vApply fs ss) 

что приятно. Обратите внимание, что длина n является чисто статической вещью в этой функции, гарантируя, что входные и выходные векторы имеют одинаковую длину, хотя эта длина не играет роли в выполнении vApply . Напротив, гораздо сложнее (т. vApply Невозможно) реализовать функцию, которая делает n копий заданного x (что было бы pure для vApply ‘s <*> )

 vReplicate :: x -> Vec nx 

потому что очень важно знать, сколько копий нужно сделать во время выполнения. Введите сингл.

 data Natty :: Nat -> * where Zy :: Natty Z Sy :: Natty n -> Natty (S n) 

Для любого продвигаемого типа мы можем построить одноэлементное семейство, проиндексированное по продвинутому типу, заseleniumное дублирующимися по времени его значениями. Natty n – тип копий времени выполнения типа n :: Nat . Теперь мы можем написать

 vReplicate :: Natty n -> x -> Vec nx vReplicate Zy x = VNil vReplicate (Sy n) x = VCons x (vReplicate nx) 

Таким образом, у вас есть значение уровня типа, вычисленное для значения времени выполнения: проверка копии времени выполнения ставит статические знания значения уровня. Несмотря на то, что термины и типы отделены друг от друга, мы можем работать в зависимости от типа, используя конструкцию синглтона как своего рода эпоксидную смолу, создавая связи между фазами. Это далеко от разрешения произвольных выражений во время выполнения в типах, но это не что иное.

Что такое противный? Чего не хватает?

Давайте немного надавить на эту технологию и посмотреть, что начинает колебаться. Мы могли бы понять, что синглтоны должны управляться немного более неявно

 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 породил три копии: вид, одноэлементное семейство и одноэлементный class. У нас довольно неуклюжий процесс для обмена явными значениями 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 mx) 

В каждом из наших двух уравнений обе стороны имеют вид Nat .

Теперь попробуйте один и тот же перевод для чего-то, индексированного по векторам.

 data InVec :: x -> Vec nx -> * where Here :: InVec z (VCons z zs) After :: InVec z ys -> InVec z (VCons y ys) 

становится

 data InVec (a :: x) (as :: Vec nx) = forall mz (zs :: Vec xm). (n ~ S m, as ~ VCons z zs) => Here | forall myz (ys :: Vec xm). (n ~ S m, as ~ VCons y ys) => After (InVec z ys) 

и теперь мы формируем эквациональные ограничения между as :: Vec nx и VCons z zs :: Vec (S m) x где обе стороны имеют синтаксически различные (но доказуемо равные) виды. Ядро GHC в настоящее время не оборудовано для такой концепции!

Что еще не хватает? Ну, большая часть Haskell отсутствует на уровне типа. Язык терминов, которые вы можете рекламировать, имеет только переменные и конструкторы, отличные от GADT. После того, как вы приобретете их, type family оборудование позволяет вам писать программы на уровне типов: некоторые из них могут быть похожими на функции, которые вы хотели бы написать на уровне термина (например, оснащение Nat дополнением, чтобы вы могли дать хороший тип append для Vec ), но это просто совпадение!

Еще одна вещь, отсутствующая на практике, – это библиотека, которая использует наши новые возможности для индексирования типов по значениям. Что делают Functor и Monad в этом смелом новом мире? Я думаю об этом, но еще многое предстоит сделать.

Запуск программ уровня

Haskell, как и большинство зависимых языков программирования, имеет две операционные семантики. Существует так, что система времени выполнения запускает программы (только закрытые выражения, после стирания стилей, сильно оптимизированы), а затем есть способ, которым программа typechecker запускает программы (ваши семейства типов, ваш «class типа Prolog» с открытыми выражениями). Для Haskell вы обычно не смешиваете два, потому что исполняемые программы находятся на разных языках. В зависимости от типизированных языков есть отдельные модели времени выполнения и статического исполнения для одного и того же языка программ, но не беспокойтесь, модель времени выполнения по-прежнему позволяет вам стирать тип и, действительно, доказательство стирания: вот что дает механизм извлечения Coq ; это, по крайней мере, то, что делает компилятор Эдвина Брэди (хотя Эдвин стирает излишне дублированные значения, а также типы и доказательства). Различие в фазе не может быть различием синтаксической категории дольше, но оно живое и здоровое.

Зависимые типизированные языки, являющиеся тотальными, позволяют программе typechecker запускать программы без страха перед чем-то хуже, чем долгое ожидание. Когда Haskell становится более навязчивым, мы сталкиваемся с вопросом о том, какова должна быть его статическая модель исполнения? Один из подходов может заключаться в том, чтобы ограничить статическое выполнение полными функциями, что позволило бы нам с той же свободой работать, но может заставить нас делать различия (по крайней мере для кода типа) между данными и кодами, чтобы мы могли определить, следует ли обеспечить прекращение или производительность. Но это не единственный подход. Мы вольны выбирать гораздо более слабую модель исполнения, которая неохотно запускает программы, ценой принятия меньших уравнений выходят только путем вычисления. Фактически, это то, что на самом деле делает GHC. Правила типизации ядра GHC не упоминают запущенные программы, но только для проверки доказательств для уравнений. При переводе на kernel ​​решатель ограничений GHC пытается запустить ваши программы на уровне уровня, создавая небольшой серебристый след доказательств того, что данное выражение равно его нормальной форме. Этот метод генерации доказательств является немного непредсказуемым и неизбежно незавершенным: например, он борется с страшной рекурсией, и это, вероятно, мудро. Единственное, о чем нам не нужно беспокоиться, это выполнение вычислений IO вывода в typechecker: помните, что typechecker не должен давать launchMissiles то же самое значение, что и во время выполнения системы!

Культура Хиндли-Милнера

Система типа Хиндли-Милнера достигает действительно удивительного совпадения четырех различных различий, с неудачным культурным побочным эффектом, который многие люди не могут видеть различие между различиями и предположить, что совпадение неизбежно! О чем я говорю?

  • термины против типов
  • явно написанные вещи против неявно написанных вещей
  • наличие во время выполнения до стирания до времени выполнения
  • не зависящая от абстракции и зависимая количественная оценка

Мы привыкли писать термины и оставлять типы для вывода … и затем стираться. Мы используем для количественной оценки переменных типа с соответствующей абстракцией типа, и приложение происходит тихо и статически.

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

Какие варианты выбора языка мы должны рассмотреть, потому что эти совпадения больше не выполняются? Например, правильно ли Haskell не дает возможности создать экземпляр forall x. t forall x. t квантификатор явно? Если typechecker не может угадать x путем unifiying t , у нас нет другого способа сказать, что должно быть x .

В более широком смысле мы не можем рассматривать «вывод типа» как monoлитную концепцию, в которой мы имеем либо все, либо ничего. Для начала нам нужно отделить «обобщающий» аспект (правило «пусть» Милнера), который в значительной степени опирается на ограничение того, какие типы существуют, чтобы гарантировать, что глупая машина может ее угадать, из аспекта «специализации» (Milner’s «var «правило»), которое так же эффективно, как и ваш решатель ограничений. Мы можем ожидать, что типы верхнего уровня станут более сложными, но информация о внутреннем типе будет довольно легко распространяться.

Следующие шаги для Haskell

Мы видим, что уровни типов и типов очень похожи (и они уже имеют внутреннее представление в GHC). Мы могли бы также объединить их. Было бы интересно принять * :: * если можно: мы давно потеряли логическую надежность, когда мы допустили дно, но тип звука обычно является более слабым требованием. Мы должны проверить. Если у нас должны быть разные типы типов, рода и т. Д., Мы можем, по крайней мере, убедиться, что все на уровне типа и выше всегда можно продвигать. Было бы здорово снова использовать polymorphism, который у нас уже есть для типов, вместо того, чтобы повторно изобретать polymorphism на уровне вида.

Мы должны упростить и обобщить существующую систему ограничений, разрешив гетерогенные уравнения a ~ b где виды a и b не являются синтаксически идентичными (но могут быть доказаны равными). Это старая техника (в моем тезисе, в прошлом столетии), благодаря которой зависимость намного легче справляется. Мы могли бы выразить ограничения на выражения в GADT и таким образом ослабить ограничения на то, что можно повысить.

Мы должны исключить необходимость создания одноэлементной конструкции путем введения зависимого типа функции pi x :: s -> t . Функция с таким типом может быть явно применена к любому выражению типа s которое живет в пересечении типов и терминов (так, переменные, конструкторы, с более поздними). Соответствующая lambda и приложение не будут стерты во время выполнения, поэтому мы сможем написать

 vReplicate :: pi n :: Nat -> x -> Vec nx vReplicate Z x = VNil vReplicate (S n) x = VCons x (vReplicate nx) 

без замены Nat Natty . Область pi может быть любым промотируемым типом, поэтому, если GADT можно продвигать, мы можем написать зависимые последовательности квантификаторов (или «телескопы», как их называл Briuijn)

 pi n :: Nat -> pi xs :: Vec nx -> ... 

до какой бы длины мы ни нуждались.

Суть этих шагов заключается в том, чтобы устранить сложность , работая непосредственно с более общими инструментами, вместо того, чтобы делать со слабыми инструментами и неуклюжими кодировками. Нынешний частичный бай-ин делает преимущества зависимых типов Haskell более дорогими, чем они должны быть.

Слишком сложно?

Зависимые типы заставляют многих людей нервничать. Они заставляют меня нервничать, но мне нравится нервничать, или, по крайней мере, мне трудно не нервничать. Но это не помогает, что вокруг этой темы есть такой туман невежества. Некоторые из них связаны с тем, что нам все еще предстоит многому научиться. Но сторонники менее радикальных подходов, как известно, ставят страх перед зависимыми типами, не всегда убедившись, что факты полностью связаны с ними. Я не буду называть имена. Эти «неразрешимые проверки типов», «Тьюринга неполные», «без разделения фаз», «стирание типа», «доказательства повсюду» и т. Д., Мифы сохраняются, хотя они мусор.

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

Ибо, как и при любом повышении артикуляции, мы можем свободно говорить о новых вещах, а также справедливых. Например, существует множество сложных способов определения двоичных деревьев поиска, но это не означает, что нет хорошего способа . Важно не предполагать, что плохие переживания не могут быть улучшены, даже если это впадает в эго, чтобы признать это. Дизайн зависимых определений – это новый навык, который требует обучения, а программист Haskell не делает вас профессиональным специалистом! И даже если некоторые программы являются фолом, почему вы лишаете других свободы быть справедливыми?

Зачем все еще беспокоиться о Haskell?

Я действительно наслаждаюсь зависимыми типами, но большинство моих проектов взлома все еще находятся в Haskell. Зачем? Haskell имеет classы типов. У Haskell есть полезные библиотеки. Haskell имеет работоспособную (хотя и далеко не идеальную) обработку программирования с эффектами. У Haskell есть компилятор промышленной силы. Зависимые типизированные языки находятся на гораздо более ранней стадии развития сообщества и инфраструктуры, но мы доберемся туда с реальным сменом поколения в возможностях, например, посредством метапрограммирования и обобщенных данных. Но вам просто нужно взглянуть на то, что люди делают в результате шагов Haskell по отношению к зависимым типам, чтобы увидеть, что есть много преимуществ, которые можно получить, продвигая вперед нынешнее поколение языков вперед.

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

 data Some :: (k -> *) -> * where Like :: px -> 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)))) 

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

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

Что касается «почему бы и нет»? вопрос, есть несколько моментов, которые я думаю. Первый момент состоит в том, что, хотя основное понятие за зависимыми типами легко (разрешить типы зависимости от значений), разветвления этого основного понятия являются как тонкими, так и глубокими. Например, различия между значениями и типами все еще живы и хороши; но обсуждение различий между ними становится намного более тонким, чем в вашем Хиндли – Милнере или Системе F. В некоторой степени это связано с тем, что зависимые типы фундаментально трудны (например, логика первого порядка неразрешима). Но я думаю, что большая проблема заключается в том, что у нас нет хорошего словарного запаса для сбора и объяснения того, что происходит. По мере того, как все больше людей узнают о зависимых типах, мы разработаем лучший словарный запас, и поэтому все станет понятнее, даже если основные проблемы еще сложны.

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

Interesting Posts

Когда следует использовать «использование» блоков в C #?

Как отключить Bluetooth-устройство с помощью Android 2.1 sdk

Как я могу объединить две коммиты в одну, если я уже начал rebase?

Не удалось найти файл объявления для модуля ‘module-name’. ‘/path/to/module-name.js’ неявно имеет тип “any”

Каков тип строковых литералов в C и C ++?

Windows с полным контролем для всех для всего

JSON.NET Parser * кажется * будет двойной сериализацией моих объектов

Затенение графика плотности ядра между двумя точками.

Каков правильный способ повторного прикрепления отдельных объектов в Hibernate?

Понимание CSS для стилей пользователя в браузере

Android: TabHost без TabActivity

Используют ли веб-браузер разные исходящие порты для разных вкладок?

Панель инструментов Android Добавление элементов меню для разных fragmentов

Fetch API с Cookie

Какую версию MS-DOS использует Rufus для создания загрузочных USB-накопителей?

Давайте будем гением компьютера.