Что за суета насчет Haskell? [закрыто]


109

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

Может ли кто-нибудь привести несколько примеров Haskell, показывающих, почему он такой элегантный / превосходный?

Ответы:


134

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

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


47
Следует отметить, что сообщений об ошибках Haskell нет, в отличие от ghc. Стандарт Haskell не определяет, как создаются сообщения об ошибках.
PyRulez

Для таких плебей, как я, GHC означает компилятор Glasgow Haskell. en.wikipedia.org/wiki/Glasgow_Haskell_Compiler
Lorem Ipsum

137

Это пример , который убедил меня , чтобы узнать Haskell (и мальчик я рад , что я и сделал).

-- program to copy a file --
import System.Environment

main = do
         --read command-line arguments
         [file1, file2] <- getArgs

         --copy file contents
         str <- readFile file1
         writeFile file2 str

Хорошо, это короткая, читаемая программа. В этом смысле это лучше, чем программа на C. Но чем это так отличается от (скажем) программы на Python с очень похожей структурой?

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

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

Как бы то ни было, Haskell понимает, что это writeFileзависит от класса readFile, и поэтому может оптимизировать этот путь к данным.

Хотя результаты зависят от компилятора, при запуске вышеуказанной программы обычно происходит следующее: программа считывает блок (скажем, 8 КБ) первого файла, затем записывает его во второй файл, а затем считывает другой блок из первого файла. файл и записывает его во второй файл и так далее. (Попробуйте запустить straceего!)

... что очень похоже на то, что делает эффективная реализация копии файла на языке C.

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

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

  1. Лучший дизайн программы. Снижение сложности приводит к меньшему количеству логических ошибок.

  2. Компактный код. Меньше строк для ошибок.

  3. Ошибки компиляции. Многие ошибки просто недопустимы для Haskell .

Haskell не для всех. Но каждый должен попробовать.


Как именно вы измените константу 8 КБ (или что-то еще)? Потому что я готов поспорить, что в противном случае реализация Haskell будет медленнее, чем версия C, особенно без предварительной выборки ...
user541686 07

1
@Mehrdad Вы можете изменить размер буфера с помощью hSetBuffering handle (BlockBuffering (Just bufferSize)).
Дэвид

3
Удивительно, что этот ответ получил 116 голосов, но то, что там написано, просто не так. Эта программа будет читать весь файл, если вы не используете ленивые байтовые строки (которые вы можете использовать Data.Bytestring.Lazy.readFile), которые не имеют ничего общего с Haskell как ленивым (нестрогим) языком. Монады секвенируются - это примерно означает, что «все побочные эффекты исчезают, когда вы убираете результат». Что касается магии "ленивых байтов": это опасно, и вы можете сделать это с похожим или более простым синтаксисом на большинстве других языков.
Джо Со

14
Скучный старый стандарт readFileтакже выполняет ленивый ввод-вывод Data.ByteString.Lazy.readFile. Так что ответ не ошибочный, и это не просто оптимизация компилятора. Действительно, это часть спецификации для Haskell : « readFileФункция читает файл и возвращает содержимое файла в виде строки. Файл читается лениво, по запросу, как и в случае с getContents
Daniel Wagner

1
Я думаю, что другие ответы указывают на более особенные вещи в Haskell. Во многих языках / средах есть потоки, вы можете сделать что-то подобное в Node : const fs = require('fs'); const [file1, file2] = process.argv.slice(2); fs.createReadStream(file1).pipe(fs.createWriteStream(file2)). У Bash тоже есть нечто похожее:cat $1 > $2
Max

64

Вы как бы задаете неправильный вопрос.

Haskell - это не язык, на котором вы смотрите на несколько классных примеров и говорите: «Ага, теперь я понял, вот что делает его хорошим!»

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

  • Ленивая оценка
  • Никаких побочных эффектов (все чисто, ввод-вывод и т.д. происходит через монады)
  • Невероятно выразительная система статических шрифтов

а также некоторые другие аспекты, которые отличаются от многих основных языков (но разделяются некоторыми):

  • функциональная
  • значительный пробел
  • предполагаемый тип

Как ответили некоторые другие плакаты, сочетание всех этих функций означает, что вы думаете о программировании совершенно по-другому. И поэтому трудно придумать пример (или набор примеров), который адекватно передал бы это Джо, программисту основного направления. Это экспериментальная вещь. (Для аналогии я могу показать вам фотографии моей поездки в Китай в 1970 году, но, посмотрев фотографии, вы все равно не узнаете, каково это было жить там в то время. Точно так же я могу показать вам Haskell 'quicksort', но вы все равно не поймете, что значит быть Haskeller.)


17
Я не согласен с вашим первым предложением. Сначала меня действительно впечатлили несколько примеров кода Haskell, и что действительно убедило меня, что стоит изучить эту статью, так это эта статья: cs.dartmouth.edu/~doug/powser.html. Но, конечно, это интересно для математика / физика. Программист, изучающий вещи из реального мира, сочтет этот пример смешным.
Рафаэль С. Кальсаверини

2
@Rafael: Возникает вопрос: «Что бы впечатлило программиста, изучающего реальный мир»?
JD

Хороший вопрос! Я не программист "реального мира", поэтому не знаю, что им нравится. хахаха ... Я знаю, что любят физики и математики. : P
Рафаэль С. Кальсаверини

27

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

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


1
Что ж, всегда есть возможность всегда и только использовать монаду ST и / или unsafePerformIOдля людей, которые просто хотят смотреть, как горит мир;)
Сара

22

Отчасти проблема заключается в том, что чистота и статическая типизация позволяют использовать параллелизм в сочетании с агрессивной оптимизацией. Параллельные языки сейчас в моде, а многоядерность немного мешает.

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

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


"вместе с быстрым компилятором нативного кода"?
JD

Я считаю, что Дон имеет в виду GHCI.
Грегори Хигли,

3
@Jon: Shotout.alioth.debian.org/u32/… Haskell, например, неплохо справляется с перестрелками .
Пикер

4
@Jon: Код перестрелки очень старый, и из далекого прошлого, когда GHC не был оптимизирующим компилятором. Тем не менее, это доказывает, что код Haskell может работать на низком уровне для повышения производительности, если это необходимо. Новые решения в перестрелке более идиоматичны и по-прежнему быстры.
Пикер

1
@GregoryHigley Есть разница между GHCI и GHC.
Джереми Лист


18

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

Возможно, самой большой победой для меня стала возможность модулировать поток управления через такие вещи, как моноиды, монады и так далее. Очень простой пример - моноид Ordering; в таком выражении, как

c1 `mappend` c2 `mappend` c3

где c1и так по возвращении LT, EQили GT, c1возвращаясь EQвызывает выражение для продолжения, оценки c2; если c2возвращается LTили GTэто значение целого и c3не оценивается. Подобные вещи становятся значительно более изощренными и сложными в таких вещах, как генераторы монадических сообщений и парсеры, где я могу переносить различные типы состояний, иметь различные условия прерывания или, возможно, захочу решить для любого конкретного вызова, действительно ли означает прерывание. «без дальнейшей обработки» или означает «вернуть ошибку в конце, но продолжить обработку для сбора дополнительных сообщений об ошибках».

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

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


2
Очень интересно! Сколько всего строк кода Haskell вошло в вашу автоматизированную торговую систему? Как вы справлялись с отказоустойчивостью и какие результаты вы получали? Недавно я подумал, что Haskell может быть хорош для программирования с низкой задержкой ...
JD

12

Для интересного примера вы можете посмотреть: http://en.literateprograms.org/Quicksort_(Haskell)

Что интересно, так это посмотреть на реализацию на разных языках.

Что делает Haskell таким интересным, как и другие функциональные языки, так это то, что вы должны иначе думать о том, как программировать. Например, вы обычно не будете использовать циклы for или while, но будете использовать рекурсию.

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


2
рекурсия - это бомба. это и сопоставление с образцом.
Эллери Новичок,

1
Когда я пишу на функциональном языке, мне труднее всего избавляться от циклов for и while. :)
Джеймс Блэк

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

8
За исключением того, что работающий программист на Haskell редко использует примитивную рекурсию; в основном вы используете библиотечные функции, такие как map и foldr.
Пол Джонсон,

18
Мне кажется более интересным то, что оригинальный алгоритм быстрой сортировки Хоара был превращен в эту неуместную форму, основанную на списках, очевидно, чтобы бесполезно неэффективные реализации могли быть написаны на Haskell «элегантно». Если вы попытаетесь написать реальную (на месте) быструю сортировку на Haskell, вы обнаружите, что это чертовски уродливо. Если вы попытаетесь написать на Haskell универсальную быструю сортировку с конкурентоспособной производительностью, вы обнаружите, что это на самом деле невозможно из-за давних ошибок в сборщике мусора GHC. Приветствую quicksort как хороший пример для Haskell не верится, ИМХО.
JD

8

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


1
Не забудьте прочитать исходный код компилятора. Это также даст вам много ценной информации.
JD

7

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

Например, если вы хотите вычислить все простые числа, вы можете использовать

primes = sieve [2..]
    where sieve (p:xs) = p : sieve [x | x<-xs, x `mod` p /= 0]

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

foo = sum $ takeWhile (<100) primes

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

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


2
Это не совсем так; с поведением yield return C # (объектно-ориентированный язык) вы также можете объявлять бесконечные списки, которые оцениваются по запросу.
Джефф Йейтс,

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

8
Возможно, вам будет интересно прочитать, почему «решето» не является Решетом
Крис Конвей,

@Chris: Спасибо, это была действительно интересная статья! Вышеупомянутая функция простых чисел не та, которую я использовал для своих собственных вычислений, поскольку она очень медленная. Тем не менее, в статье подчеркивается, что проверка всех чисел на наличие мода - это действительно другой алгоритм.
свиристель

6

Я согласен с другими, что просмотр нескольких небольших примеров - не лучший способ продемонстрировать Haskell. Но я все равно отдам. Вот молниеносное решение проблем 18 и 67 проекта Эйлера , которые просят вас найти путь с максимальной суммой от основания до вершины треугольника:

bottomUp :: (Ord a, Num a) => [[a]] -> a
bottomUp = head . bu
  where bu [bottom]     = bottom
        bu (row : base) = merge row $ bu base
        merge [] [_] = []
        merge (x:xs) (y1:y2:ys) = x + max y1 y2 : merge xs (y2:ys)

Вот полная многоразовая реализация алгоритма BubbleSearch Леша и Митценмахера. Я использовал его для безотходной упаковки больших медиафайлов для архивного хранения на DVD:

data BubbleResult i o = BubbleResult { bestResult :: o
                                     , result :: o
                                     , leftoverRandoms :: [Double]
                                     }
bubbleSearch :: (Ord result) =>
                ([a] -> result) ->       -- greedy search algorithm
                Double ->                -- probability
                [a] ->                   -- list of items to be searched
                [Double] ->              -- list of random numbers
                [BubbleResult a result]  -- monotone list of results
bubbleSearch search p startOrder rs = bubble startOrder rs
    where bubble order rs = BubbleResult answer answer rs : walk tries
            where answer = search order
                  tries  = perturbations p order rs
                  walk ((order, rs) : rest) =
                      if result > answer then bubble order rs
                      else BubbleResult answer result rs : walk rest
                    where result = search order

perturbations :: Double -> [a] -> [Double] -> [([a], [Double])]
perturbations p xs rs = xr' : perturbations p xs (snd xr')
    where xr' = perturb xs rs
          perturb :: [a] -> [Double] -> ([a], [Double])
          perturb xs rs = shift_all p [] xs rs

shift_all p new' [] rs = (reverse new', rs)
shift_all p new' old rs = shift_one new' old rs (shift_all p)
  where shift_one :: [a] -> [a] -> [Double] -> ([a]->[a]->[Double]->b) -> b
        shift_one new' xs rs k = shift new' [] xs rs
          where shift new' prev' [x] rs = k (x:new') (reverse prev') rs
                shift new' prev' (x:xs) (r:rs) 
                    | r <= p    = k (x:new') (prev' `revApp` xs) rs
                    | otherwise = shift new' (x:prev') xs rs
                revApp xs ys = foldl (flip (:)) ys xs

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

Приведя вам несколько примеров, как вы просили, я скажу, что лучший способ начать ценить Haskell - это прочитать статью Джона Хьюза, которая дала мне идеи, необходимые для написания упаковщика DVD: Почему функциональное программирование имеет значение . Эта статья на самом деле предшествует Haskell, но она блестяще объясняет некоторые идеи, которые заставляют людей любить Haskell.


5

Для меня привлекательность Haskell - это обещание гарантированной корректности компилятора . Даже если это для чистых частей кода.

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


6
Как это гарантирует правильность?
Джонатан Фишофф

Чистые части кода намного безопаснее нечистых. Уровень доверия / затраченных усилий намного выше.
rpg

1
Что произвело на вас такое впечатление?
JD

5

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

Причина в лаконичном синтаксисе и простоте тестирования.

Вот на что похож синтаксис объявления функции:

foo a = a + 5

Это самый простой способ определения функции.

Если я напишу обратное

inverseFoo a = a - 5

Я могу проверить, является ли это обратным для любого случайного ввода, написав

prop_IsInverse :: Double -> Bool
prop_IsInverse a = a == (inverseFoo $ foo a)

И вызов из командной строки

jonny @ ubuntu: runhaskell quickCheck + имена fooFileName.hs

Что проверит, что все свойства в моем файле сохранены, путем случайного тестирования входных данных сто раз.

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


Какие проблемы вы решаете и какие другие языки пробовали?
JD

1
Трехмерная графика в реальном времени для мобильных устройств и iPad.
Джонатан Фишофф

3

Если вы можете осмыслить систему типов в Haskell, я думаю, что это само по себе является большим достижением.


1
Что там получить? Если необходимо, подумайте о «данных» == «класс» и «класс типов» = «интерфейс» / «роль» / «черта». Нет ничего проще. (Нет даже "null", чтобы вас запутать. Null - это концепция, которую вы можете встроить в свой шрифт самостоятельно.)
jrockway

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

2

в нем нет циклических конструкций. не во многих языках есть эта черта.


17
ghci>: m + Control.Monad ghci> forM_ [1..3] print 1 2 3
sastanin

1

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


-1

Высказывание противоположной точки зрения: Стив Йегге пишет, что языкам Хиндели-Милнера не хватает гибкости, необходимой для написания хороших систем :

HM очень красив в совершенно бесполезном формально-математическом смысле. Он очень хорошо обрабатывает несколько вычислительных конструкций; диспетчеризация сопоставления с образцом, найденная в Haskell, SML и OCaml, особенно удобна. Неудивительно, что он в лучшем случае неудобно обрабатывает некоторые другие распространенные и очень желательные конструкции, но они объясняют эти сценарии, говоря, что вы ошибаетесь, что на самом деле они вам не нужны. Вы знаете, такие вещи, как установка переменных.

Haskell стоит изучить, но у него есть свои недостатки.


5
Хотя это, безусловно, правда, что системы строгих типов обычно требуют, чтобы вы их придерживались (это то, что делает их сильные стороны полезными), это также тот случай, когда многие (большинство?) Существующих систем типов на основе HM, на самом деле, имеют своего рода ' escape hatch ', как описано в ссылке (возьмем Obj.magic в O'Caml в качестве примера, хотя я никогда не использовал его, кроме как взломать); на практике, однако, для многих программ такое устройство никогда не требуется.
Зак Сноу,

3
Вопрос о том, является ли установка переменных «желательной», зависит от того, насколько болезненно использовать альтернативные конструкции, по сравнению с тем, сколько боли вызывает использование переменных. Это не для того, чтобы отвергнуть весь аргумент, а скорее для того, чтобы указать, что принятие утверждения «переменные - очень желательная конструкция» в качестве аксиомы не является основанием для убедительного аргумента. Просто так случилось, что большинство людей учатся программировать.
gtd

5
-1: Утверждения Стива частично устарели, но в большинстве случаев полностью неверны. Ослабленное ограничение значений OCaml и система типов .NET - очевидные контрпримеры его утверждениям.
JD

4
У Стива Йегге есть необоснованная пчела по поводу статического набора текста, и он не только неправильно говорит об этом, но и продолжает вспоминать об этом при каждой доступной возможности (и даже при некоторых недоступных). В этом отношении вам следует доверять только своему собственному опыту.
ShreevatsaR

3
Хотя я не согласен с Йегге относительно статической и динамической типизации, в Haskell есть тип Data.Dynamic. Если вам нужна динамическая типизация, вы можете ее получить!
jrockway
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.