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


204

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

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

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

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

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

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

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


31
Я позволю людям лучше, чем я, писать реальные ответы, но суть в том, что языки со статической типизацией, такие как C #, имеют систему типов, которая пытается помочь вам написать защищаемый код; системы типов, такие как попытка Хаскелла помочь вам написать правильный (т.е. доказуемый) код. Основным принципом работы является перемещение объектов, которые можно проверить на этапе компиляции; Haskell проверяет больше вещей во время компиляции.
Роберт Харви

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

12
Я не знаю, почему все ответы упоминаются Maybeтолько к концу. Если бы мне пришлось выбирать только одну вещь, которую более популярные языки должны позаимствовать у Haskell, это было бы так. Это очень простая идея (так что она не очень интересна с теоретической точки зрения), но только она сделает нашу работу намного проще.
Пол

1
Здесь будут отличные ответы, но в попытке помочь, изучите тип подписей. Они позволяют людям и программам рассуждать о программах таким образом, чтобы проиллюстрировать, как Java находится в мягких средних типах.
Майкл Пасха

6
Справедливости ради я должен отметить, что «целое, если оно скомпилируется, оно сработает» - это лозунг, а не буквальное изложение факта. Да, мы, программисты на Haskell, знаем, что прохождение проверки типов дает хорошие шансы на корректность, для некоторых ограниченных понятий правильности, но это, безусловно, не буквально и повсеместно «истинное» утверждение!
Том Эллис

Ответы:


230

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

  • Безопасность . Типы Haskell обладают довольно хорошими свойствами безопасности типов. Это довольно специфично, но по существу это означает, что значения одного типа не могут произвольно преобразовываться в другой тип. Это иногда противоречит изменчивости (см. Ограничение значения OCaml )
  • Алгебраические типы данных . Типы в Haskell имеют по существу ту же структуру, что и математика средней школы. Это невероятно просто и непротиворечиво, но, как оказалось, настолько мощно, насколько вы, возможно, захотите. Это просто отличная основа для системы типов.
    • Типовое программирование . Это не то же самое, что универсальные типы (см. Обобщение ). Вместо этого, из-за простоты структуры типов, как отмечалось ранее, относительно легко написать код, который работает в общем случае над этой структурой. Позже я расскажу о том, как что-то вроде Equality может быть получено автоматически для пользовательского типа компилятором Haskell. По сути, способ, которым это происходит, состоит в том, чтобы пройтись по общей простой структуре, лежащей в основе любого пользовательского типа, и сопоставить ее между значениями - очень естественная форма структурного равенства.
  • Взаимно рекурсивные типы . Это просто необходимый компонент написания нетривиальных типов.
    • Вложенные типы . Это позволяет вам определять рекурсивные типы по переменным, которые используются в разных типах. Например, один тип сбалансированных деревьев data Bt a = Here a | There (Bt (a, a)). Тщательно продумайте действительные значения Bt aи обратите внимание, как работает этот тип. Это сложно!
  • Обобщение . Это слишком глупо, чтобы этого не было в системе типов (хм, глядя на тебя, иди). Важно иметь представление о переменных типа и умение говорить о коде, который не зависит от выбора этой переменной. Хиндли Милнер - это система типов, которая получена из Системы F. Система типов Хаскелла является разработкой типизации HM, а Система F по сути является сердцем обобщения. Я хочу сказать, что у Хаскелла очень хорошая обобщающая история.
  • Абстрактные типы . История Хаскелла здесь не велика, но и не существует. Можно написать типы, которые имеют открытый интерфейс, но частную реализацию. Это позволяет нам одновременно допускать изменения в коде реализации и, что важно, поскольку это основа всех операций в Haskell, писать «магические» типы, которые имеют четко определенные интерфейсы, такие как IO. Честно говоря, в Java, вероятно, действительно есть более приятная история абстрактного типа, но я не думаю, что пока Интерфейсы не станут более популярными, это действительно было правдой.
  • Параметричность . Значения Haskell не имеют каких - либо универсальных операций. Java нарушает это такими вещами, как ссылочное равенство и хэширование, и еще более грубо - принуждением Это означает, что вы получаете бесплатные теоремы о типах, которые позволяют вам в значительной степени узнать значение операции или значения по ее типу - некоторые типы таковы, что может быть только очень небольшое число жителей.
  • Типы с более высоким родом обнаруживают все типы при кодировании более сложных вещей. Functor / Applicative / Monad, Foldable / Traversable, вся mtlсистема ввода эффектов, обобщенные фиксированные точки функторов. У этого списка нет конца. Есть много вещей, которые лучше всего выражаются в более высоких видах, и относительно немного систем типов даже позволяют пользователю говорить об этих вещах.
  • Типы занятий . Если вы думаете о системах типов как о логике - что полезно - тогда от вас часто требуется что-то доказать. Во многих случаях это, по сути, линейный шум: правильный ответ может быть только один, и программисту это напрасная трата времени и усилий. Классы типов - это способ для Haskell сгенерировать доказательства для вас. Говоря более конкретно, это позволяет вам решать простые «системы уравнений типа», такие как «По какому типу мы собираемся (+)все вместе? О Integer, хорошо! Давайте сейчас добавим правильный код!». В более сложных системах вы можете устанавливать более интересные ограничения.
    • Исчисление ограничений . Ограничения в Haskell, которые являются механизмом проникновения в систему прологов классов типов, структурно типизированы. Это дает очень простую форму отношений подтипов, которая позволяет собирать сложные ограничения из более простых. Вся mtlбиблиотека основана на этой идее.
    • Выведение . Чтобы управлять каноничностью системы классов типов, необходимо написать много часто тривиального кода, чтобы описать ограничения, которые должны быть созданы пользовательскими типами. Применительно к очень нормальной структуре типов Haskell, часто можно попросить компилятор сделать этот шаблон для вас.
    • Тип класса пролог . Решатель классов типа Хаскелла - система, которая генерирует те «доказательства», о которых я упоминал ранее, - по сути, ограниченная форма Пролога с более хорошими семантическими свойствами. Это означает, что вы можете кодировать действительно волосатые вещи в типологическом прологе и ожидать, что они будут обработаны во время компиляции. Хорошим примером может служить решение для доказательства того, что два гетерогенных списка эквивалентны, если вы забудете о порядке - это эквивалентные гетерогенные «наборы».
    • Многопараметрические классы типов и функциональные зависимости . Это просто очень полезные улучшения базового пролога типов. Если вы знаете Пролог, вы можете представить, насколько увеличивается выразительная сила, когда вы можете писать предикаты более чем одной переменной.
  • Довольно хороший вывод . Языки, основанные на системах типов Хиндли Милнера, имеют довольно хороший вывод. Сам HM имеет полный вывод, что означает, что вам никогда не нужно писать переменную типа. Haskell 98, самая простая форма Haskell, уже выбрасывает это в некоторых очень редких случаях. Вообще, современный Haskell был экспериментом по медленному сокращению пространства полного вывода, добавляя больше мощности к HM и отслеживая жалобы пользователей. Люди очень редко жалуются - вывод Хаскелла довольно хорош.
  • Только очень, очень, очень слабый подтип . Ранее я упоминал, что система ограничений из пролога класса типов имеет понятие структурного подтипа. Это единственная форма подтипов в Хаскеле . Подтипирование ужасно для рассуждений и умозаключений. Это значительно усложняет каждую из этих проблем (система неравенств вместо системы равенств). Это также очень легко неправильно понять (Подклассы - это то же самое, что и подтипы? Конечно, нет! Но люди очень часто путают это, и многие языки помогают в этой путанице! Как мы оказались здесь? Полагаю, никто никогда не проверял LSP.)
    • Обратите внимание, что недавно (в начале 2017 года) Стивен Долан опубликовал свою диссертацию по MLsub , варианту вывода типа ML и Hindley-Milner, который имеет очень хорошую историю подтипов ( см. Также ). Это не отменяет того, что я написал выше - большинство систем подтипов сломаны и имеют неверный вывод, - но на самом деле предполагает, что мы только сегодня могли найти несколько многообещающих способов, позволяющих полностью совместить вывод и подтип. Теперь, чтобы быть совершенно ясным, понятия субтипирования в Java никоим образом не могут использовать преимущества алгоритмов и систем Долана. Это требует переосмысления того, что означает подтип.
  • Типы высшего ранга . Я говорил об обобщении ранее, но больше , чем просто простое обобщение , что это полезно , чтобы иметь возможность говорить о типах , которые обобщенно переменные в них . Например, отображение между структурами более высокого порядка, которое не замечает (см. Параметричность ) того, что эти структуры «содержат», имеет такой тип (forall a. f a -> g a). В прямом HM вы можете написать функцию в этом типе, но с типами высокого ранга вы требуете такой функции в качестве аргумента , как так: mapFree :: (forall a . f a -> g a) -> Free f -> Free g. Обратите внимание, что aпеременная связана только внутри аргумента. Это означает, что определитель функции mapFreeдолжен решать, что aименно создается при его использовании, а не пользователь mapFree.
  • Экзистенциальные типы . В то время как типы более высокого ранга позволяют нам говорить об универсальной квантификации, экзистенциальные типы позволяют говорить об экзистенциальной квантификации: идея, что просто существует некоторый неизвестный тип, удовлетворяющий некоторым уравнениям. Это оказывается полезным, и для того, чтобы продолжать в том же духе, потребуется много времени.
  • Тип семьи . Иногда механизмы классов типов неудобны, так как мы не всегда думаем в Прологе. Семейства типов позволяют нам писать прямые функциональные отношения между типами.
    • Семей закрытого типа . Семейства типов по умолчанию открыты, что раздражает, поскольку это означает, что, хотя вы можете расширять их в любое время, вы не можете «инвертировать» их, не надеясь на успех. Это потому, что вы не можете доказать инъективность , но с семьями закрытого типа вы можете.
  • Добрые проиндексированные типы и продвижение типов . В этот момент я становлюсь очень экзотичным, но время от времени они имеют практическое применение. Если вы хотите написать тип дескрипторов, которые открыты или закрыты, вы можете сделать это очень хорошо. Обратите внимание, что в следующем фрагменте Stateприведен очень простой алгебраический тип, значения которого также повышены до уровня типа. Затем, впоследствии, мы можем говорить о конструкторах типов, например, о том, Handleчто они принимают аргументы определенного типа, например State. Это сбивает с толку, чтобы понять все детали, но также очень правильно.

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

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

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

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


7
Нуль не был «ошибкой» в миллиард долларов. Существуют случаи, когда невозможно статически проверить, что указатель не будет разыменован до того, как какое-либо значимое может существовать ; попытка разыменования в таком случае зачастую лучше, чем требование, чтобы указатель первоначально идентифицировал бессмысленный объект. Я думаю, что самой большой ошибкой, связанной с нулем, было наличие реализаций, которые, учитывая char *p = NULL;, будут ловушкой *p=1234, но не ловушкой char *q = p+5678;ни*q = 1234;
суперкат

37
Это просто цитата из Тони Хоара: en.wikipedia.org/wiki/Tony_Hoare#Apologies_and_retractions . Хотя я уверен, что бывают моменты, когда nullэто необходимо в арифметике указателей, я вместо этого интерпретирую, что говорить, что арифметика указателей - это плохое место для размещения семантики вашего языка, а не то, что нуль не является ошибкой.
Дж. Абрахамсон

18
@supercat, вы действительно можете написать язык без нуля. Разрешить это или нет - это выбор.
Пол Дрэйпер

6
@supercat - эта проблема также существует в Haskell, но в другой форме. Haskell обычно ленив и неизменен, и поэтому позволяет писать p = undefinedдо тех пор, пока pон не оценен. Более полезно, вы можете вставить undefinedкакую-то изменчивую ссылку, опять же, если вы ее не оцениваете. Более серьезная проблема связана с ленивыми вычислениями, которые могут не завершиться, что, конечно, неразрешимо. Основное отличие состоит в том, что все они однозначно программируют ошибки и никогда не используются для выражения обычной логики.
Кристиан Конкль,

6
В @supercat Haskell полностью отсутствует семантика ссылок (это понятие прозрачности ссылок, которое подразумевает, что все сохраняется путем замены ссылок их ссылками). Таким образом, я думаю, что ваш вопрос некорректен.
Дж. Абрахамсон

78
  • Полный тип вывода. На самом деле вы можете использовать сложные типы повсеместно, не чувствуя себя как «Святое дерьмо, все, что я когда-либо делаю, это пишу подписи типов».
  • Типы являются полностью алгебраическими , что позволяет очень легко выразить некоторые сложные идеи.
  • У Haskell есть классы типов, которые похожи на интерфейсы, за исключением того, что вам не нужно размещать все реализации для одного типа в одном месте. Вы можете создавать реализации своих собственных классов типов для существующих сторонних типов без необходимости доступа к их источнику.
  • Высокоуровневые и рекурсивные функции имеют тенденцию добавлять больше функций в область проверки типов. Возьмите фильтр , например. В императивном языке вы можете написать forцикл для реализации той же функциональности, но у вас не будет тех же гарантий статического типа, потому что forцикл не имеет понятия возвращаемого типа.
  • Отсутствие подтипов значительно упрощает параметрический полиморфизм.
  • Типы с более высоким родом (типы типов) относительно легко задавать и использовать в Haskell, который позволяет создавать абстракции вокруг типов, которые абсолютно непостижимы в Java.

7
Хороший ответ - не могли бы вы дать мне простой пример более высокого типа, подумайте, это поможет мне понять, почему это невозможно сделать в Java.
phatmanace

3
Есть несколько хороших примеров здесь .
Карл Билефельдт

3
Сопоставление с образцом также очень важно, это означает, что вы можете использовать тип объекта, чтобы принимать решения очень легко.
Бенджамин Грюнбаум

2
@ BenjaminGruenbaum Не думаю, что я бы назвал это функцией системы типов.
Доваль

3
Хотя ADT и HKT, безусловно, являются частью ответа, я сомневаюсь, что любой, кто задает этот вопрос, узнает, почему они полезны, я предлагаю расширить оба раздела, чтобы объяснить это
jk.

62
a :: Integer
b :: Maybe Integer
c :: IO Integer
d :: Either String Integer

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

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


11
+1 пока этот ответ не полный, я думаю, что он гораздо лучше разбирается на уровне вопроса
jk.

1
+1 Хотя это помогло бы объяснить, что другие языки также имеют Maybe(например, Java Optionalи Scala Option), но в этих языках это недоделанное решение, так как вы всегда можете назначить nullпеременную этого типа, и ваша программа взорвется во время выполнения. время. Этого не может случиться с Haskell [1], потому что нет нулевого значения , поэтому вы просто не можете обмануть. ([1]: на самом деле, вы можете сгенерировать ошибку, аналогичную NullPointerException, используя частичные функции, например, fromJustкогда у вас есть Nothing, но эти функции, вероятно, не одобряются).
Андрес Ф.

2
«целое число, значение которого пришло из внешнего мира» - не будет ли IO Integerэто ближе к «подпрограмме, которая при выполнении дает целое число»? Поскольку а) main = c >> cзначение, возвращаемое первым, cможет отличаться от второго, в cто время как aбудет иметь одно и то же значение независимо от его положения (если мы находимся в одной области видимости). Б) существуют типы, которые обозначают значения из внешнего мира для обеспечения его санации. (т.е. не помещать их напрямую, но сначала проверить, является ли ввод от пользователя правильным / не злонамеренным).
Мацей Пехотка

4
Мацей, это было бы точнее. Я стремился к простоте.
WolfeFan

30

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

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

foobar :: x -> y -> y

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

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

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

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

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

Как видите, сигнатура типа Haskell говорит о многом!


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

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

Этот метод может сделать несколько вещей:

  • Вернуть in2как результат.
  • Цикл навсегда и никогда ничего не вернуть.
  • Брось исключение и никогда ничего не возвращай.

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

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

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

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


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

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

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


4
+1 Отличный ответ! Это очень сильно и часто недооценивается новичками в Haskell. Кстати, более простая «невозможная» функция была бы fubar :: a -> b, не так ли? (Да, я в курсе unsafeCoerce. Я предполагаю, что мы не говорим ни о чем с «небезопасным» в своем названии, и новичкам не стоит об этом беспокоиться!: D)
Андрес Ф.

Есть много простых типов подписей, которые вы не можете написать, да. Например, foobar :: xдовольно нереализуемый ...
MathematicalOrchid

На самом деле, вы не можете сделать чистый код небезопасным, но вы все равно можете сделать его многопоточным. Вы можете выбрать следующие варианты: «прежде чем оценивать это, оценить это», «когда вы оцениваете это, вы можете также оценить это в отдельном потоке», и «когда вы оцениваете это, вы можете также оценить это, возможно, в отдельной теме ". По умолчанию «делай как хочешь», что по сути означает «оценивать как можно позже».
Джон Дворак

Более прозаично, вы можете вызывать методы экземпляра в in1 или in2, которые имеют побочные эффекты. Или вы можете изменить глобальное состояние (которое предоставляется, моделируется как действие ввода-вывода в Haskell, но может не соответствовать тому, что большинство людей воспринимает как IO).
Даг МакКлин

2
@isomorphismes Тип x -> y -> yотлично реализуем. Типа (x -> y) -> yнет. Тип x -> y -> yпринимает два входа и возвращает второй. Тип (x -> y) -> yберет функцию, которая работает x, и каким-то образом должен сделать yиз этого ...
MatumaticOrchid

17

Схожий вопрос ТАК .

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

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

String x = (String)someNonString;

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

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

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

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

data Tree a = Leaf a | Branch (Tree a) (Tree a) 

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

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


Я понял только исключения NullPointerException из вашего ответа. Не могли бы вы включить больше примеров?
Джесвин Хосе

2
Не обязательно верно, JLS §5.5.1 : Если T является типом класса, то либо | S | <: | T | или | T | <: | S |. В противном случае возникает ошибка времени компиляции. Таким образом, компилятор не позволит вам приводить необратимые типы - очевидно, есть способы обойти это.
Борис Паук

На мой взгляд, самый простой способ использовать преимущества классов типов состоит в том, что они подобны классам interfaces, которые могут быть добавлены после факта, и они не «забывают» тип, который их реализует. То есть вы можете гарантировать, что два аргумента функции имеют одинаковый тип, в отличие от interfaces, где два List<String>s могут иметь разные реализации. Технически вы можете сделать что-то очень похожее в Java, добавив параметр типа к каждому интерфейсу, но 99% существующих интерфейсов этого не делают, и вы запутаете своих коллег.
Доваль

2
@BoristheSpider Верно, но приведение исключений почти всегда подразумевает даункастинг из суперкласса в подкласс или из интерфейса в класс, и для суперкласса это не редкость Object.
Доваль

2
Я думаю, что вопрос в вопросе о строках связан не с ошибками приведения типов и типов во время выполнения, а с тем фактом, что если вы не хотите использовать типы, Java не заставит вас - как в действительности, хранить ваши данные в сериализованном виде. форма, злоупотребляя строками как специальный anyтип. Хаскелл тоже не остановит тебя, потому что ... ну, у него есть струны. Haskell может дать вам инструменты, он не может принудительно помешать вам делать глупости, если вы настаиваете на том, чтобы Гринспеннинг достаточно интерпретатора, чтобы заново изобретать его nullво вложенном контексте. Ни один язык не может.
Леушенко

0

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

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

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

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

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

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

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

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

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

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

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

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

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.