Pull to refresh

Comments 52

Хорошая статья. Я вот присматривался одно время к Haskell, но потом по мере погружения в Common Lisp куда-то потерялась мотивация… Понял что меня в CL, по существу, всё устраивает, и пока нет нужды учить что-то новое. Наверное, то же можно сказать и про Haskell.
Спасибо, как раз сейчас учу хаскель и хочется разбираться с ним на основе проекта
Под кривой обучения обычно понимают не время на то чтобы написать свой велик, а время, через которое становится понятен чужой.

Есть ли в проекте другие участники? Они все могут добавить в «большой стек трансформеров монад» и ещё одну? Кто-нибдуь может вточить хоть какую-нибудь фичу в проект пока Вы в отпуске?
Под кривой обучения обычно понимаю не время на то, чтобы написать свой велик, а время на то, чтобы понять чужой.

В проекте есть другие люди? Они могут «добавить в большой стек трансформеров монад ещё одну монаду»? Кто-нибудь может добавить в проект хоть какую-нибудь фичу, пока Вы в отпуске?
Других участников проекта нет.

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

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

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

Я ни в коей мере не подвергаю сомнению отдачу от внедрения Haskell в вашем проекте. Но в «одно лицо» можно изучить и успешно внедрить всё, что угодно, при условии получения фана этим лицом.
Можете задать этот вопрос в списке рассылки haskell-cafe.

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

Код верхнего уровня понимают даже не разработчики, т.е. можно интуитивно догадаться, что он делает, т.к. фактически это DSL на предметной области
Что за проект, если не секрет, и каковы успехи?
Заказная разработка, демон для управления специфическим железом + месседжинг по сети. Ну как успехи? Планомерно делается, сделается — сдастся. В принципе, проект будет довольно заметный, закроем проект — расскажу. Никаких проблем от хаскелла нет.
А пишете с блэкджеком и трансформерами как и автор поста? Были ли проекты на хаскеле до этого у обоих? Как изучали всё это мясо Control.Monad?
Конечно. А как без трансформеров, если нужно вложить несколько монад?

Control.Monad — это штука, сделанная для нашего с вами удобства, как ее не использовать?

Это мой второй и его первый коммерческий проект на haskell
Я понял — вы оба уже на 80м уровне =)
Да тут нет ничего сложного. Думайте о монаде, как о контексте.

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

А матан можно оставить для любителей — для того, что бы писать на haskell, он необязателен.
Может быть вы не с того конца начали изучать?

После прочтения нескольких руководств и вхождения в тему большинство библиотек становятся поятными. И это мясо обычно не учат, а используют как справочник. А ещё чаще как справочник используют hoogle или hayoo и особо не запоминают какие функции и типы в каких модулях лежат.
А какая разница?

Живой пример. Открываем гитхаб, вбиваем слово «haskell» в поиск, кликаем в первый проект с понятной целью. Листаем исходники… И что видим? Видим какой-то ёбаный страх. На то чтобы вкурить сигнатуру любого из этих смайликов у меня уходит мин 10. Ещё мин 20 я пытаюсь соотнести её с окружающими выражениями и понять что оно делает всё вместе. Полтора часа на то, чтобы понять одну строчку кода. Их там 400, пускай собственно кода треть — 130. Получаем минимальное время на то, чтобы внести изменение в модуль ~ 4 недели. Ок, йа табуретко, считаем я в 4 раза тупее среднего разработчика. Один багфикс в неделю от свежего человека. На сколько? Пока каждый смайлик не попадётся раз 20 (для меня 100, но помним про табуретку), это три таких бага. Это даже не смешно, это 03 и принудительная госпитализация.
Конкретно этот пример требует понимания только типов данных, монад, библиотеки parsec и аппликативных функторов. Точнее даже не аппликативных функторов, а операторов <$> и <*> из Control.Applicative. Они делают простые вещи.
-- | Parses a BInt
getBInt :: Get BCode
getBInt = BInt . read <$> getWrapped 'i' 'e' intP
    where intP = ((:) <$> char '-' <*> getDigits) <|> getDigits

Ключевой момент здесь — тип Get a. Что это такое, я не знаю, в этом модуле не определён. Нужно найти откуда он импортируется и понять что он точно делает. По коду он похож на монаду парсинга, такую как в parsec.

getBInt :: Get BCode

Сигнатура функции говорит, что в этом месте из какого-то потока токенов, может быть строки символов, парсится значение типа BCode.
Описание типа BCode есть в этом модуле.

BInt . read

Это бесточечная нотация www.haskell.org/haskellwiki/Pointfree.
Вот сигнатура оператора (.):
(.) :: (b -> c) -> (a -> b) -> a -> c   -- Defined in GHC.Base

Эта нотация как-то связана с комбинаторной логикой, но я не в куре математических тонкостей.
Оператор просто принимает на вход две функции и передаёт выход второй на вход первой. Получается новая функция, которая принимает на вход то, что принимает на вход вторая функция. Для удобства можно было бы записать так:
(BInt . read)

Да, BInt — это один из конструкторов типа BCode. Это функция, которая принимает на вход Integer и возвращает BCode.

<$>

Это то же самое, что fmap. Принимает на вход чистую функцию и функцию с побочным зффектом. Сначала вычисляет функцию с побочным эффектом, берёт оттуда чистый результат, подаёт на вход чистой функции. Результат всего этого делает побочным.
getWrapped 'i' 'e' intP

Здесь происходит сам парсинг с побочным эффектом.

(:) -- оператор добавления элемента в начало списка
<$> <*> -- операторы из Control.Applicative, позволяют удобно комбинировать чистые функции и функции с эффектами.
<|> -- оператор выбора альтернатив парсера, такой же как в parsec.

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

getWrapped 'i' 'e' intP

Это функция, которая делает из парсера intP новый парсер. Путём добавления в начало и в конец двух символов для парсинга.

-- | Get something wrapped in two Chars
getWrapped :: Char -> Char -> Get a -> Get a
getWrapped a b p = char a *> p <* char b

Операторы (*>) и (<*) я никогда раньше не использовал и не знал что такое. Берём hoogle www.haskell.org/hoogle/, вводим там (*>). Попадаем в документацию к модулю Control.Applicative:
(*>) :: f a -> f b -> f b
Sequence actions, discarding the value of the first argument.

(<*) :: f a -> f b -> f a
Sequence actions, discarding the value of the second argument. 

Всё ясно, будем знать.

Итог: 20 секунд на понимание кода и 10 минут на этот текст. Какие недели, о чём вы?

Надеюсь, не стоит приводить примеры из c++, c#, python, perl, которые произведут точно такое же впечатление на первый взгляд?
Сурово… ПОпробую вечером осилить.
Немного ещё поразбирался, библиотеки парсеров здесь не используются. Есть только аппликативные функторы и Serialize — извлечение значений из байтовых потоков. Но смысл тот же.
Да не переживайте Вы так. Я правда верю, что можно писать на хаскеле просто и понятно (только не видно, чтобы кто-то так делал). Я даже верю в то, что в местах, вроде того что я привёл кто-то может разбираться.

Но порог из требуемых усилий (таланта?) для понимания такого кода даже для меня, в некоторой мере любителя разных ЯП, после полугода (достаточно ленивого надо признать) ковыряния самого Хаскеля и порта его библиотек на Скалу не преодолим.
Что-то я увлёкся кодом.

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

Как быстро вольётся — сложный вопрос. Но есть же разные участки кода по сложности. Самые простые вещи, думаю, можно начать писать очень быстро.
Хорошая статья, спасибо!

Не подскажете, приходилось ли работать с кодировками отличными от UTF-8? Последний раз, когда я играл с Хаскеллом это было большой проблемой, расширение encoding не собиралось и ничего не работало.

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

Как измерить в цифрах все эти показатели?

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

Я представлял что-то в этом роде:

То есть для этой оценки нужно всё переписать на С++ или на другом сравниваемом языке, а потом сравнить?
Даже если это сделать, то условия уже будут другие.
Нет, зачем переписывать. Можно сравнить с существующими примерами решения аналогичных задач на других языках.

Просто без такого сравнения говорить о преимуществах немного преждевременно.
Ну не обязательно всё вносить с таблицу. Человек, много писавший на Си++, а затем попробовавший достаточно много писать на Хаскеле, чисто субъективно заметит разницу.
Если интересуют измерения, то код со сплайновой геометрией на Haskell занял у меня 500 строк, на C++ 3к+. Ошибки и добавление функциональности сравнивать, к сожалению, конкретно в этом примере смысла нет, так как на Haskell был написан и отлажен прототип. И хотя доказательств для данного случая привести не могу, чисто субъективно REPL на порядок ускорил написание кода — можно было код исправлять почти на ходу и смотреть работоспособность.

Я неоднократно слышал от различных людей, что если код на Haskell компилируется, то он работает (это, конечно, явное преувеличение, но имеется в виду эффект в сравнении с другим языком, обычно C++). Так что можно сделать примерный вывод о времени, потраченном на исправление багов.
О, спасибо! Это уже интереснее в плане статистики.

Не подскажете, Вы один работали над кодом или в команде? Производительность в итоге была одинаковой?
Писал один. Большим плюсом стоит отметить произвольный порядок определений, благодаря чему математические формулы (f =… x… y, где x = ..., y = ...) переписываются напрямую, в связи с чем есть основание полагать, что он достаточно понятен будет любому мало-мальски знакомому с Haskell программисту. Я вообще люблю писать именно в таком стиле, потому что после foo = пишется самое основное, а после where постепенно определяется всё остальное в порядке важности.

Производительность никогда не мерил (хотя, безусловно, было бы интересно попробовать), так как этот прототип был для уверенности, что я нигде не напортачил. Т.е. я написал, отладил, а потом большую часть времени переписывал это на C++, в основном занимаясь переводом всяких map и foldr.

Чтобы не лукавить, в целом настолько разительное отличие (аж 6 крат) в кол-ве строк встречается не так часто, это зависит как от задачи, так и от умения программиста пользоваться обобщённым кодом. Т.е. если сразу сесть после C++ на Haskell, то, например IO, скорее всего будет занимать столько же, так как по привычке будет переписано в лоб.
С кодировками сейчас проблем нет.
encodeString CP1251 . decodeString UTF8
Спасибо за статью. При переходе на Хаскель я постепенно я всё больше убеждался, что некоторые якобы недостатки на самом деле лишь достоинства: отсутствие перегрузки; разделение именования типов, конструкторов и функций; неизменяемые значения.
А эффект «компилируется значит работает» производит неизгладимое впечатление даже сейчас.
> Сторонники питона утверждают, что для него написаны уже почти все библиотеки. Но мне приходилось участвовать в одном питоновском проекте, в котором был с нуля переписан существующий пакет для доступа к сишной библиотеке. Потому что существующий код был ужасен. Стало быть это преимущество питона не так уж велико.

Странная логика.
> Значительно увеличенная производительность программиста.
> Более лаконичный, понятный код, более дешёвый в сопровождении.
> Меньше ошибок, более надёжные программы.
> Меньше «семантическая пропасть» между программистом и языком.
> Более быстрый выпуск продукта.

Сравнительные степени как-бэ требуют двух сравниваемых предметов. Всё это быстрее и лучше относительно чего? NASM? C? C++? Java? Python? Common Lisp? (на самом деле порядок случайный, а не то что вы подумали :P)
Относительно традиционных языков программирования.
Перечислите традиционные языки программирования, пожалуйста :)
Чем характеризуются “традиционные языки программирования”?

(Серьёзно интересно посмотреть на набор характеристик под который подойдут все перечисленные мной языки, чтобы их можно было как одну сущность сравнивать, а не по отдельности)
Может термин «традиционные» не очень удачен.

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

Некоторые ещё называют их императивными, противопоставляя их декларативным, таким как SQL, Haskell, язык Makefile и т.п.
Спасибо за статью.
P.S. Скажите, вы всегда меряете объем проекта количеством строк?
К слову, попытался подтянуть encoding. Под Windows по-прежнему не собирается — как и ранее требуется файл system_encoding.h.

stackoverflow.com/questions/4701585/how-to-install-haskell-module-encoding-0-6-3-on-windows — тут есть советы о том, как это можно поправить, но оба способа выглядят некрасиво.
> Порадовало, что программы на хаскеле собираются даже для таких проверенных временем систем, как Red Hat Enterprise Linux 5

А собрать .deb сильно сложнее, чем .rpm? А то хотел взглянуть ваш проект на Убунте по-быстрому, а…
Наверное даже легче, в дебиане и убунту поддержка хаскеля получше чем в рэдхатовских дистрибутивах.

Просто пока дело не дошло.

Но можно воспользоваться пакетом tar.gz, там есть make install и даже make uninstall.
Не надо драматизировать. Хаскелл это круто (особенно в реальных проектах), но кидаться гавном в питон — уж извините.
Критика питона вообще не подразумевалась.

Да, там больше библиотек. И не все библиотеки качественные. Но в хаскеле ещё меньше библиотек и также хватает шлака.

По поводу работы на старых системах — очень часто системные администраторы жалуются что программисты приложений и сервисов хотят всё более новые и новые интерпретаторы. Это приводит к определённым проблемам.

К слову, у меня одна хаскельная программа на centos5 в xen периодически зависала, в dmesg писались ошибки «4gb seg fixup». Перепробовал все советы, так и не вылечил. Пришлось ставить другую систему.
А в какой IDE писался проект, если не секрет? А то меня на пути изучения хаскеля останавливает только отсутсвие удобных (уровня VisualStudio или NetBeans) сред разработки…
Проект писался в vim.

Для хаскеля существует IDE Leksah. Говорят, очень продвинутая штука.

Также есть плагин для эклипса.

Не знаю правда как они соотносятся с уровнем Visual Studio. Посмотреть подробнее не было времени.
К сожалению ни то, ни другое очень сильно не дотягивают по удобству использования до того, что я хочу, в Leksah у меня даже автодополнение не завелось…
При разработке на С++ я пользовался Visual Studio с плагином Visual Assist X.

На мой взгляд писать на хаскеле в простом редакторе удобнее, чем писать на С++ с использованием этих средств.
Забыл уточнить, говоря про VisualStudio, я имел в виду VisualStudio для С#… Я, например, уже слишком привык к автодополнению, удобной навигации, инструментам для рефакторинга и отладчику, в редакторе буду работать на несколько порядков медленнее…
Да, приходилось однажды писать на С#. Впечатления — можно вообще ничего не запоминать, автодополнение пишет код само.

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

В редакторе на несколько порядков медленнее, а в leksah уже не на несколько порядков.
Писал, хоть и немного, в функциональном стиле на Common Lisp в Emacs + slime и на F# все в той же VS — удобно, leksah — нет, так что дело не в парадигме… В общем или leksah такой, или я его готовить не умею…
Sign up to leave a comment.

Articles