Что именно делает систему типа Haskell настолько почитаемой (vs say, Java)?

Я начинаю изучать Haskell . Я очень новичок в этом, и я просто читаю пару онлайн-книг, чтобы осветить его основные конструкции.

Один из «мемов», о котором часто говорили знакомые люди, - это целая «если она скомпилирует, она будет работать» - вещь, которая, я думаю, связана с силой системы типов.

Я пытаюсь понять, почему точно Haskell лучше, чем другие статически типизированные языки в этом отношении.

Другими словами, я предполагаю, что на Java вы можете сделать что-то отвратительное, как похоронить ArrayList <String> (), чтобы содержать что-то, что действительно должно быть ArrayList <Animal> (). Отвратительная вещь здесь заключается в том, что ваша строка содержит слона, жирафа и т. Д., И если кто-то помещает Mercedes - ваш компилятор не поможет вы.

Если I сделал сделать ArrayList <Animal> (), то в какой-то более поздний момент, если я решу, что моя программа isn ' Действительно, о животных, о транспортных средствах, я могу изменить, скажем, функцию, которая создает ArrayList <Animal> для создания ArrayList <Vehicle>, и моя IDE должна сказать мне везде есть разрыв компиляции.

Мое предположение состоит в том, что это то, что люди подразумевают под системой strong , но для меня это не очевидно, почему Haskell лучше. Иными словами, вы можете написать хорошую или плохую Java, я полагаю, вы можете сделать то же самое в Haskell (то есть вещи в строки /int, которые действительно должны быть первоклассными типами данных).

Я подозреваю, что у меня отсутствует что-то важное /основное.
Я был бы очень рад, если бы показал ошибку моих путей!

193 голоса | спросил phatmanace 16 PMpThu, 16 Apr 2015 21:38:49 +030038Thursday 2015, 21:38:49

6 ответов


218

Вот неупорядоченный список системных функций типа, доступных в Haskell, и либо недоступный, либо менее приятный на Java (насколько мне известно, это, по общему признанию, слабая w.r.t. Java)

  • <Сильный> Безопасность . Типы Haskell обладают довольно хорошими свойствами «безопасности типа». Это довольно специфично, но по сути это означает, что значения в каком-то типе не могут просто превращаться в другой тип. Это иногда несовместимо с изменчивостью (см. Ограничение значения для OCaml )
  • Алгебраические типы данных . Типы в Haskell имеют по существу ту же структуру, что и математика средней школы. Это возмутительно простой и последовательный, но, как оказалось, настолько мощный, насколько вы могли этого захотеть. Это просто отличная основа для системы типов.
    • Программирование общего типа данных . Это не то же самое, что и общие типы (см. обобщение ). Вместо этого из-за простоты структуры типа, как указано выше, довольно легко написать код, который работает в целом по этой структуре. Позже я расскажу о том, как что-то вроде Eq uality может быть автоматически выведено для пользовательского типа компилятором Haskell. По существу способ, которым он это делает, - это пройти через общую, простую структуру, лежащую в основе любого пользовательского типа, и сопоставить ее между ценностями - очень естественную форму структурного равенства.
  • Взаимно рекурсивные типы . Это просто важный компонент написания нетривиальных типов.
    • Вложенные типы . Это позволяет вам определять рекурсивные типы по переменным, которые рекурсируют на разных типах. Например, один тип сбалансированных деревьев - это data Bt a = Здесь a | Там (Bt (a, a)). Подумайте о допустимых значениях Bt a и обратите внимание, как работает этот тип. Это сложно!
  • <Сильный> Обобщение . Это почти слишком глупо, чтобы не иметь в системе типов (гм, глядя на вас, Go). Важно иметь понятия переменных типа и способность говорить о коде, который не зависит от выбора этой переменной. Hindley Milner - это система типов, которая получена из системы System F. Haskell - это разработка HM-типирования, а система F по существу является очагом обобщения. Я хочу сказать, что у Haskell есть история очень хорошая .
  • Абстрактные типы . Рассказ Хаскелла здесь невелик, но также не существует. Можно писать типы, которые имеют открытый интерфейс, но частную реализацию. Это позволяет нам как позднее вносить изменения в код реализации, так и, что важно, поскольку это основа всей операции в Haskell, пишите «магические» типы, которые имеют четко определенные интерфейсы, такие как IO. Разумеется, Java, вероятно, имеет более красивую абстрактную историю, но я не думаю, что до тех пор, пока интерфейсы не стали более популярными, это было действительно правдой.
  • <Сильный> Parametricity . Значения Haskell не имеют универсальных операций any . Java нарушает это с помощью таких вещей, как эталонное равенство и хеширование и еще более грубо с принуждением. Это означает, что вы получаете бесплатные теоремы о типах, которые позволяют вам знать значение операции или значение в значительной степени полностью из своего типа - некоторые типы таковы, что может быть только очень небольшое количество жителей.
  • Типы более высокого качества отображают все типы при кодировании более сложных вещей. Functor /Applicative /Monad, Foldable /Traversable, целая система ввода текста mtl, обобщенные контрольные точки функтора. У этого списка нет конца. Есть много вещей, которые лучше всего выражаются в более высоких видах, и относительно немного типов систем даже позволяют пользователю говорить об этих вещах.
  • Типы классов . Если вы думаете о типах систем как о логике, что полезно, то вас часто требуют доказать. Во многих случаях это, по сути, линейный шум: может быть только один правильный ответ, и это пустая трата времени и усилий, чтобы программист это сказал. Typeclasses - это способ для Haskell генерировать доказательства для вас. Более конкретно, это позволяет вам решать простые «системы уравнений типа», такие как «В каком типе мы собираемся объединить вещи (+)? Oh, Integer, ok! Давайте теперь введем правильный код! ». В более сложных системах вы можете создавать более интересные ограничения.
    • Исчисление ограничений . Ограничения в Haskell, которые являются механизмом для входа в систему пролога typeglass, являются структурно типизированными. Это дает очень простую форму отношения подтипирования, которая позволяет собирать сложные ограничения из более простых. Вся эта библиотека mtl основана на этой идее.
    • <Сильный> Выводя . Чтобы управлять системой canonity системы типов, необходимо написать много часто тривиального кода, чтобы описатьограничения, определяемые пользовательскими типами. Сделайте до самой нормальной структуры типов Haskell, часто можно попросить компилятор сделать этот шаблон для вас.
    • Тип класса пролог . Решатель класса типа Haskell - система, которая генерирует те «доказательства», о которых я упоминал ранее, - по существу является искалеченной формой Пролога с более хорошими семантическими свойствами. Это означает, что вы можете кодировать действительно волосатые вещи в прологе типа и ожидать, что они будут обработаны во время компиляции. Хорошим примером может быть решение для доказательства того, что два гетерогенных списка эквивалентны, если вы забыли о порядке - они эквивалентны гетерогенным «наборам».
    • Классы с несколькими параметрами и функциональные зависимости . Это просто массовые полезные уточнения для базового пролога. Если вы знаете Prolog, вы можете себе представить, насколько выразительная сила увеличивается, когда вы можете писать предикаты более чем одной переменной.
  • Довольно хороший вывод . Языки, основанные на системах типа Hindley Milner, имеют довольно хороший вывод. Сам HM имеет вывод complete , что означает, что вам никогда не нужно писать переменную типа. Haskell 98, простейшая форма Haskell, уже выбрасывает это в некоторых очень редких случаях. Как правило, современный Haskell был экспериментом по медленному сокращению пространства полного вывода, добавляя больше мощности для HM и наблюдая, как пользователи жалуются. Люди очень редко жалуются: «Вывод Хаскелла довольно хорош.
  • Очень, очень, очень слабый подтипирование только . Я упоминал ранее, что система ограничений из пролога прорисов имеет понятие структурного подтипирования. Это единственная форма подтипирования в Haskell . Подтипирование ужасно для рассуждений и умозаключений. Это делает каждую из этих проблем значительно сложнее (система неравенств вместо системы равенств). Это также очень легко понять неправильно (подклассифицируется так же, как подтипирование? Конечно, нет! Но люди очень часто путают это, и многие языки помогают в этой путанице! Как мы оказались здесь? Я полагаю, никто никогда не рассматривает LSP.)
  • <Литий>
    • Примечание недавно (начало 2017 года) Стивен Долан опубликовал свою диссертацию в MLsub , вариант ML и Hindley-Milner вывода типа очень приятный ( см. также ). Это не устраняет то, что я написал выше, - большинство систем подтипирования сломаны и имеют плохое умозаключение, - но предполагает, что мы только сегодня обнаружили некоторые перспективные способы полного вывода и подтипирования. Теперь, чтобы быть полностью ясными, понятия Java о подтипинге никоим образом не могут использовать алгоритмы и системы Долана. Это требует переосмысления того, что означает подтипирование.
  • Типы более высокого ранга . Раньше я говорил об обобщении, но больше, чем просто простое обобщение, полезно иметь возможность говорить о типах, которые имеют обобщенные переменные внутри них . Например, сопоставление между структурами более высокого порядка, которые не обращают внимания (см. параметричность ) на то, что эти структуры содержат, имеет тип типа (forall a. Fa - gt; ga) . В прямом HM вы можете написать функцию этого типа, но с типами более высокого ранга вы требуете такую ​​функцию, как аргумент , например: mapFree :: (forall a. Fa -> ga) -> Свободный f -> Бесплатно g. Обратите внимание, что переменная a связана только внутри аргумента. Это означает, что определитель функции mapFree получает решение о том, что создается a при использовании, а не пользователь mapFree.
  • Экзистенциальные типы . В то время как типы более высокого ранга позволяют говорить о универсальной квантификации, экзистенциальные типы позволяют говорить об экзистенциальной квантификации: идея о существовании неизвестного типа, удовлетворяющего некоторым уравнениям. Это заканчивается тем, что полезно, и продолжать дольше об этом потребуется много времени.
  • Типы семейств . Иногда механизмы typeclass неудобны, поскольку мы не всегда думаем в Prolog. Типы типов позволяют писать прямые функциональные отношения между типами.
    • Закрытые типы семейств . Типы типов по умолчанию открыты, что раздражает, потому что это означает, что, хотя вы можете продлить их в любое время, вы не сможете «инвертировать» их с какой-либо надеждой на успех. Это связано с тем, что вы не можете доказать инъективность , но с закрытыми семействами типов вы можете.
  • Виды индексированных типов и продвижение типов . На данный момент я становлюсь очень экзотичным, но время от времени они имеют практическое применение. Если вы хотите написать типручек, которые являются открытыми или закрытыми, тогда вы можете сделать это очень красиво. Обратите внимание на следующий фрагмент, что State - очень простой алгебраический тип, который также повысил его значения на уровне типа. Затем мы можем поговорить о конструкторах , таких как Handle, как принимать аргументы в определенных types , например State. Сложно понять все детали, но также и очень правильно.

    data Состояние = Открыть | Закрыто
    
    data Handle :: State -> * -> * где
      OpenHandle :: {- something -} -> Ручка Откройте
      ClosedHandle :: {- something -} -> Ручка закрыта a
    
  • Представления типа времени выполнения, которые работают . Java известна тем, что имеет стирание стилей и обладание этим эффектом на некоторых парадах людей. Тип erasure есть правильный путь, однако, как будто у вас есть функция getRepr :: a -> TypeRepr, то вы, по крайней мере, нарушите параметричность. Хуже всего то, что если это созданная пользователем функция, которая используется для запуска небезопасных принуждений во время выполнения ... тогда у вас есть серьезная проблема безопасности . Системная система Typeable Haskell позволяет создать безопасный coerce :: (Typeable a, Typeable b) => a -> Возможно, b. Эта система основана на Typeable, реализованном в компиляторе (а не на пользовательской), а также не может быть предоставлена ​​такая хорошая семантика без механического механизма Haskell и законы, за которыми это гарантировано.

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

  • <Сильный> Чистота . Haskell не дает побочных эффектов для очень, очень и очень широкого определения «побочного эффекта». Это заставляет вас вводить больше информации в типы, поскольку типы управляют входами и выходами и без побочных эффектов все должны учитываться на входах и выходах.
    • <Сильный> IO . Впоследствии Haskell нуждался в способе говорить о побочных эффектах - так как любая настоящая программа должна включать в себя некоторые из них, поэтому сочетание типов типов, более высоких типов типов и абстрактных типов породило понятие использования особого особо особого типа, называемого IO a для представления побочных вычислений, которые приводят к значениям типа a. Это основа хорошей системы эффектов very , встроенной в чистый язык.
  • Недостаток null . Всем известно, что null - ошибка миллиарда долларов современных языков программирования. Алгебраические типы, в частности способность просто добавлять состояние «не существует» к типам, которые у вас есть, преобразуя тип A в тип Maybe A, полностью смягчить проблему null.
  • Полиморфная рекурсия . Это позволяет вам определять рекурсивные функции, которые обобщают переменные типа, несмотря на то, что они используют их для разных типов в каждом рекурсивном вызове в своем собственном обобщении. Об этом трудно говорить, но особенно полезно говорить о вложенных типах. Вернитесь к типу Bt a и попробуйте написать функцию для вычисления ее размера: size :: Bt a -> Int. Это будет немного похоже на size (Здесь a) = 1 и size (There bt) = 2 * size bt. Оперативно это не слишком сложно, но обратите внимание, что рекурсивный вызов size в последнем уравнении встречается в другом типе , но общее определение имеет хороший обобщенный тип < code> size :: Bt a -> Int. Обратите внимание, что это функция, которая разбивает общий вывод, но если вы предоставляете подпись типа, то Haskell разрешит ее.

Я мог продолжать идти, но этот список должен начать вас, а затем - некоторые.

ответил J. Abrahamson 17 AMpFri, 17 Apr 2015 05:09:50 +030009Friday 2015, 05:09:50
78
  • Вывод полного типа. На самом деле вы можете использовать сложные типы повсеместно, не чувствуя себя так: «Святое дерьмо, все, что я когда-либо делаю, - это записи типа записи».
  • Типы полностью алгебраические , что позволяет очень легко выразить некоторые сложные идеи.
  • Haskell имеет классы типов, которые являются подобными интерфейсами, за исключением того, что вам не нужно размещать все реализации для одного типа в одном и том же месте. Вы можете создавать реализации ваших собственных классов типов для существующих сторонних типов без необходимости доступа к их источнику.
  • Функции более высокого порядка и рекурсии имеют тенденцию вносить больше функциональности в сферу контроля проверки типов. Например, фильтр . На императивном языке вы можете написать цикл для для реализации той же функциональности, но у вас не будет одинаковых гарантий статического типа, потому что цикл для не имеет понятия возвращаемого типа.
  • Отсутствие подтипов значительно упрощает параметрический полиморфизм.
  • Более высокие типы типов (типы типов) относительно просты в определении и использовании в Haskell, что позволяет создавать абстракции вокруг типов, которые совершенно непостижимы в Java.
ответил Karl Bielefeldt 16 PMpThu, 16 Apr 2015 23:01:00 +030001Thursday 2015, 23:01:00
61
a :: Целое число
b :: Возможно Integer
c :: IO Integer
d :: Либо строковое целое

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

(Однако вы можете опустить объявления типов. В большинстве случаев компилятор может определить наиболее общий тип для ваших переменных, что приведет к успешной компиляции. Разве это не так?)

ответил WolfeFan 16 PMpThu, 16 Apr 2015 23:39:53 +030039Thursday 2015, 23:39:53
27

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

Рассмотрим следующую функцию Haskell:

foobar :: x -> y -> Y

Для реализации этой функции существует буквально только один из возможных способов . Просто подписи типа я могу сказать точно , что делает эта функция, потому что есть только одна возможная вещь, которую она может делать. [ОК, не совсем, но почти!]

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

Рассмотрим эту функцию:

fubar :: Int -> (x -> y) -> Y

Эта функция невозможна . Вы не сможете реализовать эту функцию. Я могу сказать это только из сигнатуры типа.

Как вы можете видеть, подпись типа Haskell сообщает вам многое!


Сравнение с C #. (Извините, моя Java немного ржавая.)

public static TY foobar <TX, TY> (TX in1, TY in2)

Есть несколько вещей, которые мог бы сделать этот метод:

  • В результате получим in2.
  • Петля навсегда и никогда ничего не вернет.
  • Выбросить исключение и никогда ничего не возвращать.

Собственно, у Haskell есть три варианта. Но C # также предоставляет дополнительные возможности:

  • Возвращает null. (Haskell не имеет значения null.)
  • Измените in2 перед его возвратом. (Haskell не имеет модификации на месте.)
  • Используйте отражение. (Haskell не имеет отражения.)
  • Выполните несколько операций ввода-вывода перед возвратом результата. (Haskell не позволит вам выполнять ввод-вывод, если вы не заявляете, что здесь выполняете ввод-вывод.)

Отражение - особенно большой молот; используя отражение, я могу построить новый объект TY из воздуха и вернуть его! Я могу проверить оба объекта и делать разные действия в зависимости от того, что я нахожу. Я могу сделать произвольные модификации для обоих переданных объектов.

I /O - аналогичный большой молот. Код может отображать сообщения пользователю или открывать соединения с базой данных или переформатировать ваш жесткий диск или что-либо еще.


Функция Haskell foobar, напротив, может только брать некоторые данные и возвращать эти данные без изменений. Он не может «смотреть» на данные, поскольку его тип неизвестен во время компиляции. Он не может создавать новые данные, потому что ... ну, как вы создаете данные любого возможного типа? Для этого вам понадобится размышление. Он не может выполнять какие-либо операции ввода-вывода, потому что подпись типа не заявляет, что выполняется ввод-вывод. Таким образом, он не может взаимодействовать с файловой системой или сетью или даже запускать потоки в одной и той же программе! (I.e., он на 100% гарантирован от поточности).

Как вы можете видеть, благодаря not , позволяющему вам делать целую кучу вещей, Haskell позволяет вам очень сильно гарантировать, что ваш код действительно делает. На самом деле, настолько плотно, что (для действительно полиморфного кода) обычно существует только один возможный способ, чтобы части могли совмещаться.

(Чтобы быть ясным: все еще можно писать функции Haskell, где подпись типа не говорит вам много. Int -> Int может быть примерно чем угодно. Но даже тогда мы знаем что один и тот же ввод всегда будет выдавать тот же результат со 100% уверенностью. Java даже не гарантирует этого!)

ответил MathematicalOrchid 17 PMpFri, 17 Apr 2015 14:58:25 +030058Friday 2015, 14:58:25
17

Связанный вопрос SO .

  

Я предполагаю, что вы можете сделать то же самое в haskell (т. е. вещи в строки /int, которые действительно должны быть первоклассными типами данных)

Нет, вы действительно не можете - по крайней мере, не так, как Java. В Java такое происходит:

Строка x = (String) someNonString;

, и Java с удовольствием попробует и добавит ваш не-String в виде строки. Haskell не позволяет такого рода вещи, устраняя целый класс ошибок времени выполнения.

null является частью системы типов (как Nothing), поэтому необходимо явно запрашивать и обрабатывать, устраняя целый класс ошибок времени выполнения.

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

В основном, тем не менее, это связано с тем, что система типов Haskell допускает большую выразительность. Вы можете сделать много всего лишь с несколькими правилами. Рассмотрим вездесущее дерево Haskell:

data Дерево a = Лист a | Филиал (Дерево a) (Дерево a)

Вы определили целое двоичное дерево (и два конструктора данных) в довольно читаемой одной строке кода. Все просто используют несколько правил ( типы сумм и типы продуктов ). Это 3-4 файла кода и классы в Java.

Особенно среди тех, кто склонен к почитанию систем типа, эта краткость /элегантность высоко ценится.

ответил Telastyn 16 PMpThu, 16 Apr 2015 21:54:19 +030054Thursday 2015, 21:54:19
0
  

Один из «мемов», о котором часто говорили знакомые люди, - это целая «если она скомпилирует, она будет работать» - вещь, которая, я думаю, связана с силой системы типов.

Это в основном относится к небольшим программам. Haskell запрещает вам делать ошибки, которые легко выполняются на других языках (например, сравнение Int32 и Word32 и что-то взрывается), но это не мешает вам совершать все ошибки.

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

  

Я пытаюсь понять, почему именно Haskell лучше других статически типизированных языков в этом отношении.

Типы в Haskell довольно легкие, так как легко объявлять новые типы. Это в отличие от языка, такого как Rust, где все просто немного громоздко.

  

Мое предположение состоит в том, что это то, что люди понимают под сильной системой типов, но для меня это не очевидно, почему Haskell лучше.

Haskell имеет множество функций, помимо простых сумм и типов продуктов; он имеет универсально квантифицированные типы (например, id :: a -> a). Вы также можете создавать типы записей, содержащие функции, которые сильно отличаются от языка, такого как Java или Rust.

GHC также может выводить некоторые экземпляры только на основе типов, а с момента появления дженериков вы можете писать функции, которые являются общими для типов. Это довольно удобно, и в Java оно более свободно, чем на Java.

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

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

  

Другими словами, вы можете написать хорошую или плохую Java, я предполагаю, что вы можете сделать то же самое в Haskell

Это, пожалуй, правда, но в нем отсутствует решающий момент: точка, в которой вы начинаете стрелять себе в ногу в Haskell, идет дальше, чем точка, когда вы начинаете стрелять себе в ногу на Java.

ответил 13 AMpFri, 13 Apr 2018 09:44:13 +030044Friday 2018, 09:44:13

Похожие вопросы

Популярные теги

security × 330linux × 316macos × 2827 × 268performance × 244command-line × 241sql-server × 235joomla-3.x × 222java × 189c++ × 186windows × 180cisco × 168bash × 158c# × 142gmail × 139arduino-uno × 139javascript × 134ssh × 133seo × 132mysql × 132