Я начинаю изучать Хаскель . Я очень новичок в этом, и я просто читаю пару онлайн-книг, чтобы разобраться с его основными конструкциями.
Один из «мемов», о которых часто говорили знакомые с ним люди, - это целая вещь «если она скомпилируется, она будет работать», - я думаю, что это связано с силой системы типов.
Я пытаюсь понять, почему именно Хаскель лучше, чем другие статически типизированные языки в этом отношении.
Другими словами, я предполагаю, что в Java можно сделать что-то отвратительное, например, похоронить,
ArrayList<String>()
чтобы содержать то, что действительно должно быть ArrayList<Animal>()
. Отвратительная вещь в том, что вы string
содержите elephant, giraffe
и т. Д., И если кто-то вставит Mercedes
- ваш компилятор вам не поможет.
Если бы я это сделал, ArrayList<Animal>()
то в какой-то более поздний момент времени, если я решу, что моя программа на самом деле не о животных, а о транспортных средствах, то я могу изменить, скажем, функцию, которая производит ArrayList<Animal>
для производства, ArrayList<Vehicle>
и моя IDE должна сообщать мне везде это перерыв компиляции.
Я предполагаю, что это то, что люди подразумевают под сильной системой типов, но для меня не очевидно, почему Хаскелл лучше. Иными словами, вы можете написать хорошую или плохую Java, я предполагаю, что вы можете делать то же самое в Haskell (то есть, складывать вещи в строки / целые, которые действительно должны быть первоклассными типами данных).
Я подозреваю, что мне не хватает чего-то важного / основного.
Я был бы очень рад, если бы мне показали ошибки моих путей!
источник
Maybe
только к концу. Если бы мне пришлось выбирать только одну вещь, которую более популярные языки должны позаимствовать у Haskell, это было бы так. Это очень простая идея (так что она не очень интересна с теоретической точки зрения), но только она сделает нашу работу намного проще.Ответы:
Вот неупорядоченный список функций системы типов, доступных в Haskell и недоступных или менее приятных в Java (насколько мне известно, что по общему признанию слаб по сравнению с Java)
Eq
uality может быть получено автоматически для пользовательского типа компилятором Haskell. По сути, способ, которым это происходит, состоит в том, чтобы пройтись по общей простой структуре, лежащей в основе любого пользовательского типа, и сопоставить ее между значениями - очень естественная форма структурного равенства.data Bt a = Here a | There (Bt (a, a))
. Тщательно продумайте действительные значенияBt a
и обратите внимание, как работает этот тип. Это сложно!IO
. Честно говоря, в Java, вероятно, действительно есть более приятная история абстрактного типа, но я не думаю, что пока Интерфейсы не станут более популярными, это действительно было правдой.mtl
система ввода эффектов, обобщенные фиксированные точки функторов. У этого списка нет конца. Есть много вещей, которые лучше всего выражаются в более высоких видах, и относительно немного систем типов даже позволяют пользователю говорить об этих вещах.(+)
все вместе? ОInteger
, хорошо! Давайте сейчас добавим правильный код!». В более сложных системах вы можете устанавливать более интересные ограничения.mtl
библиотека основана на этой идее.(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
. Это сбивает с толку, чтобы понять все детали, но также очень правильно.Представления типа времени выполнения, которые работают . Java славится тем, что стирает шрифты, а на парадах некоторых людей эта особенность идет под дождь. Стирание типа - правильный путь, однако, как будто у вас есть функция,
getRepr :: a -> TypeRepr
вы по крайней мере нарушаете параметрическость. Хуже всего то, что если это пользовательская функция, которая используется для запуска небезопасных мер принуждения во время выполнения ... тогда у вас есть серьезная проблема безопасности .Typeable
Система Haskell позволяет создавать сейфcoerce :: (Typeable a, Typeable b) => a -> Maybe b
. Эта система опирается на то,Typeable
что реализована в компиляторе (а не в пользовательской среде), и ей также не может быть предоставлена такая хорошая семантика без механизма классов типов Haskell и законов, которым она гарантированно будет следовать.Более того, ценность системы типов Haskell также связана с тем, как типы описывают язык. Вот несколько особенностей Haskell, которые определяют ценность через систему типов.
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 разрешит это.Я мог бы продолжать, но этот список должен помочь вам начать, а потом и немного.
источник
char *p = NULL;
, будут ловушкой*p=1234
, но не ловушкойchar *q = p+5678;
ни*q = 1234;
null
это необходимо в арифметике указателей, я вместо этого интерпретирую, что говорить, что арифметика указателей - это плохое место для размещения семантики вашего языка, а не то, что нуль не является ошибкой.p = undefined
до тех пор, покаp
он не оценен. Более полезно, вы можете вставитьundefined
какую-то изменчивую ссылку, опять же, если вы ее не оцениваете. Более серьезная проблема связана с ленивыми вычислениями, которые могут не завершиться, что, конечно, неразрешимо. Основное отличие состоит в том, что все они однозначно программируют ошибки и никогда не используются для выражения обычной логики.for
цикл для реализации той же функциональности, но у вас не будет тех же гарантий статического типа, потому чтоfor
цикл не имеет понятия возвращаемого типа.источник
В Haskell: целое число, целое число, которое может быть нулевым, целое число, значение которого пришло из внешнего мира, и целое число, которое может быть строкой, - все это различные типы - и компилятор будет применять это . Вы не можете скомпилировать программу на Haskell, которая не соблюдает эти различия.
(Однако вы можете опустить объявления типов. В большинстве случаев компилятор может определить наиболее общий тип для ваших переменных, что приведет к успешной компиляции. Разве это не аккуратно?)
источник
Maybe
(например, JavaOptional
и ScalaOption
), но в этих языках это недоделанное решение, так как вы всегда можете назначитьnull
переменную этого типа, и ваша программа взорвется во время выполнения. время. Этого не может случиться с Haskell [1], потому что нет нулевого значения , поэтому вы просто не можете обмануть. ([1]: на самом деле, вы можете сгенерировать ошибку, аналогичную NullPointerException, используя частичные функции, например,fromJust
когда у вас естьNothing
, но эти функции, вероятно, не одобряются).IO Integer
это ближе к «подпрограмме, которая при выполнении дает целое число»? Поскольку а)main = c >> c
значение, возвращаемое первым,c
может отличаться от второго, вc
то время какa
будет иметь одно и то же значение независимо от его положения (если мы находимся в одной области видимости). Б) существуют типы, которые обозначают значения из внешнего мира для обеспечения его санации. (т.е. не помещать их напрямую, но сначала проверить, является ли ввод от пользователя правильным / не злонамеренным).Многие люди перечислили хорошие вещи о Haskell. Но в ответ на ваш конкретный вопрос «почему система типов делает программы более правильными?», Я подозреваю, что ответ «параметрический полиморфизм».
Рассмотрим следующую функцию Haskell:
Существует буквально только один возможный способ реализовать эту функцию. Просто по сигнатуре типа я могу точно сказать, что делает эта функция, потому что есть только одна возможная вещь, которую она может сделать. [Хорошо, не совсем, но почти!]
Остановись и подумай об этом на мгновение. Это действительно большое дело! Это означает, что если я напишу функцию с этой сигнатурой, для нее фактически невозможно сделать что-либо кроме того, что я намеревался. (Сама подпись типа, конечно, все еще может быть неправильной. Ни один язык программирования не сможет предотвратить все ошибки.)
Рассмотрим эту функцию:
Эта функция невозможна . Вы буквально не можете реализовать эту функцию. Я могу сказать это только из сигнатуры типа.
Как видите, сигнатура типа Haskell говорит о многом!
Сравните с C #. (Извините, моя Java немного ржавая.)
Этот метод может сделать несколько вещей:
in2
как результат.На самом деле, у Haskell есть и эти три варианта. Но C # также дает вам дополнительные опции:
in2
прежде чем вернуть его. (Haskell не имеет модификации на месте.)Отражение - это особенно большой молоток; используя отражение, я могу построить новый
TY
объект из воздуха и вернуть его! Я могу осмотреть оба объекта и выполнять различные действия в зависимости от того, что я нахожу. Я могу сделать произвольные модификации для обоих объектов, переданных в.I / O - такой же большой молоток. Код может отображать сообщения пользователю, или открывать соединения с базой данных, или переформатировать ваш жесткий диск, или что-то еще.
Функция Haskell
foobar
, напротив, может принимать только некоторые данные и возвращать эти данные без изменений. Он не может «смотреть» на данные, потому что его тип неизвестен во время компиляции. Он не может создавать новые данные, потому что ... ну, как вы строите данные любого возможного типа? Для этого тебе понадобится рефлексия. Он не может выполнить какой-либо ввод-вывод, потому что подпись типа не объявляет, что ввод-вывод выполняется. Поэтому он не может взаимодействовать с файловой системой или сетью, или даже с запущенными потоками в одной программе! (То есть это 100% гарантированный потокобезопасный.)Как вы можете видеть, не позволяя вам делать кучу вещей, Haskell позволяет вам давать очень сильные гарантии того, что на самом деле делает ваш код. На самом деле настолько узкий, что (для действительно полиморфного кода) обычно есть только один возможный способ, которым части могут совмещаться.
(Чтобы быть ясным: все еще можно писать функции на Haskell, где сигнатура типа не говорит о многом.
Int -> Int
Может быть, о чем угодно. Но даже тогда мы знаем, что один и тот же ввод всегда будет давать один и тот же вывод со 100% -ной достоверностью. Java даже не гарантирует этого!)источник
fubar :: a -> b
, не так ли? (Да, я в курсеunsafeCoerce
. Я предполагаю, что мы не говорим ни о чем с «небезопасным» в своем названии, и новичкам не стоит об этом беспокоиться!: D)foobar :: x
довольно нереализуемый ...x -> y -> y
отлично реализуем. Типа(x -> y) -> y
нет. Типx -> y -> y
принимает два входа и возвращает второй. Тип(x -> y) -> y
берет функцию, которая работаетx
, и каким-то образом должен сделатьy
из этого ...Схожий вопрос ТАК .
Нет, вы действительно не можете - по крайней мере, не так, как Java. В Java такое происходит:
и Java с радостью попытается преобразовать вашу не-строку в строку. Haskell не допускает такого рода вещей, устраняя целый класс ошибок времени выполнения.
null
является частью системы типов (asNothing
), поэтому должен явно запрашиваться и обрабатываться, устраняя целый другой класс ошибок времени выполнения.Есть также множество других тонких преимуществ - особенно в отношении повторного использования и классов типов - которые я не обладаю достаточным опытом, чтобы знать достаточно хорошо, чтобы общаться.
Главным образом, хотя, это потому, что система типов Хаскелла позволяет много выразительности. Вы можете сделать много всего с несколькими правилами. Рассмотрим вездесущее дерево Haskell:
Вы определили целое двоичное дерево (и два конструктора данных) в одну читаемую строку кода. Все только по нескольким правилам (с указанием типов сумм и типов товаров ). Это 3-4 кода файлов и классов в Java.
Этот тип краткости / элегантности особенно ценится среди тех, кто склонен почитать системы типов.
источник
interface
s, которые могут быть добавлены после факта, и они не «забывают» тип, который их реализует. То есть вы можете гарантировать, что два аргумента функции имеют одинаковый тип, в отличие отinterface
s, где дваList<String>
s могут иметь разные реализации. Технически вы можете сделать что-то очень похожее в Java, добавив параметр типа к каждому интерфейсу, но 99% существующих интерфейсов этого не делают, и вы запутаете своих коллег.Object
.any
тип. Хаскелл тоже не остановит тебя, потому что ... ну, у него есть струны. Haskell может дать вам инструменты, он не может принудительно помешать вам делать глупости, если вы настаиваете на том, чтобы Гринспеннинг достаточно интерпретатора, чтобы заново изобретать егоnull
во вложенном контексте. Ни один язык не может.Это в основном верно для небольших программ. Haskell не позволяет вам совершать ошибки, которые легко допустить на других языках (например, сравнивать
Int32
и aWord32
и что-то взрывается), но это не предотвращает вас от всех ошибок.Haskell действительно делает рефакторинг намного проще. Если ваша программа ранее была правильной, и она проверяла тип, есть большая вероятность, что она будет правильной даже после незначительных исправлений.
Типы в Haskell довольно легкие, так как легко объявлять новые типы. Это в отличие от такого языка, как Rust, где все немного более громоздко.
Haskell имеет много функций, помимо простых сумм и типов продуктов; он также имеет универсально выраженные типы (например
id :: a -> a
). Вы также можете создавать типы записей, содержащие функции, которые сильно отличаются от таких языков, как Java или Rust.GHC также может извлекать некоторые экземпляры, основываясь только на типах, и с появлением обобщений вы можете писать функции, которые являются обобщенными для типов. Это довольно удобно и более свободно, чем то же самое в Java.
Другое отличие состоит в том, что Haskell имеет тенденцию иметь относительно хорошие ошибки типа (по крайней мере, на момент написания). Вывод типов в Haskell сложен, и довольно редко вам нужно будет предоставлять аннотации типов, чтобы что-то компилировать. Это отличается от Rust, где вывод типа может иногда требовать аннотаций, даже когда компилятор в принципе может определить тип.
Наконец, у Haskell есть классы типов, среди которых знаменитая монада. Монады оказываются особенно хорошим способом обработки ошибок; они в основном дают вам почти все удобства
null
без ужасной отладки и без потери безопасности вашего типа. Таким образом, способность писать функции для этих типов имеет большое значение, когда мы поощряем их использовать!Возможно, это правда, но в нем отсутствует важный момент: точка, с которой вы начинаете стрелять себе в ступню в Хаскеле, находится дальше, чем точка, в которой вы начинаете стрелять в ступню в Java.
источник