Haskell взаимодействие с другими языками. Редактирование исходного кода

Рефакторинге, TDD, багтрекерах, и даже (наконец-то!) о моем любимом Haskell. К сожалению, тема «чем же так хорош этот ваш Haskell» не была в должной мере раскрыта. Такое чувство, что большинство айтишников действительно не понимают плюсов функционального программирования. В этой заметке я постарался развернуто описать, за что лично мне нравится Haskell.

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

1. Красота и декларативность языка

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

Грустный и несчастный Java-программист, который вынужден писать всякие private static final transient volatile List> dramaticallyTerribleList = new ArrayList>; когда где-то совсем рядом есть чудесный Haskell // «о себе» одного хабраюзера

Приведу немного цифр. Описание языка в «Справочнике по языку Haskell» Романа Душкина занимает всего лишь 100 страниц. Остальные 430 страниц занимает описание модулей, которое мне еще ни разу не понадобилось. Объем книги Learn You a Haskell for Great Good! (кстати, скоро будет издан русский перевод) составляет 400 страниц. Лично мне кажется, что при желании ее можно ужать вдвое, ничего при этом не потеряв. Для сравнения, Язык программирования Си Кернигана и Ритчи имеет объем 300 страниц, а Язык программирования C++ Страуструпа — почти 1100 страниц. Вы действительно считаете сложным язык, описание которого даже по самым скромным оценкам имеет примерно тот же объем, что и описание языка Си, которое в свою очередь в 3-4 раза короче описания языка C++ ?

Тут самое время привести немного кода, продемонстрировав, как любят выражаться некоторые программисты, «высокую декларативность» языка. Например, вот так задается функция, возвращающая список делителей некого числа num:

getDivisors num
| num < 1 =
| otherwise = [ x | x <- [ 1 .. num] , num `mod ` x == 0 ]
-- ^ очевидная оптимизация -

Прямо как написать определение! Если обозначить множество делителей числа n, как D(n), и перевести написанное выше на язык математики, то получим D(n) = { x | x ∈ ℤ + , x ≤ n, mod(n, x) = 0 } , где n ∈ ℤ + . А теперь давайте напишем функцию проверки числа на простоту:

isPrime num
| num <= 1 = False
| otherwise = getDivisors num == [ 1 , num]

И снова — как будто пишешь математическую формулу. Теперь получим список всех простых чисел:

allPrimeNumbers = [ 2 ] ++ filter isPrime [ 3 , 5 .. ]

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

Если думаете, что «высокая декларативность» Haskell распространяется только на математические задачи, то вы ошибаетесь. Посмотрите, к примеру, как выглядит код GUI-приложения на Haskell . Или как к базе данных с помощью библиотеки HaskellDB. Фактически, в запросе используется обычная реляционная алгебра. Преимущество перед SQL здесь заключается в том, что корректность запроса проверяется на этапе компиляции программы. Впрочем, если хотите писать запросы на обычном SQL , никто не будет вам препятствовать.

А еще борцы против оператора goto будут рады узнать, что в Haskell его нет.

2. Автоматическое управление памятью

Haskell полностью берет управление памятью на себя. Цитата из WikiBooks :

Функциональные языки освобождают программиста от этой непростой обязанности. Память выделяется неявно, автоматически, а специальный сборщик мусора (garbage collector) возвращает системе неиспользуемые куски памяти. Оптимальные алгоритмы управления памятью сложны, но сегодня уже достаточно хорошо проработаны. Использование этих алгоритмов не сильно увеличивают время работы программы в целом (в сравнении с тем, когда программист сам, по-умному, занимается выделением и освобождением памяти).

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

Кстати, далеко не все из перечисленных проблем по-настоящему решены в таких высокоуровневых языках, как Java, Perl и Python (о проблемах C++ я вообще молчу). Например, в книге Дэвида Бизли приводится пример программы (в моем экземпляре — на стр 174), использующей паттерн «наблюдатель» , в которой сборщик мусора не в состоянии освободить память, если только не воспользоваться weakptr.

Люди, 21-ый век на дворе! Будем еще 90 лет управлять памятью с помощью костылей типа счетчиков ссылок и weakptr, а то и вообще вручную? Или наконец забудем, как страшный сон, и будем двигаться дальше?

3. Чистые функции

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

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

Вот что написано в книге Роберта Мартина Чистый код — создание, анализ и рефакторинг по поводу побочных эффектов:

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

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

Дело в том, что в Haskell функции поделены на чистые и «грязные», то есть недетерминированные и имеющие побочные эффекты. «Грязные» функции используются для ввода данных, передачи их в чистые функции и вывода результата. Другими словами, существует эдакий небольшой процедурный мирок внутри чистого функционального языка. Или наоборот, это еще как посмотреть. Все гениальное — просто!

4. Быстродействие

Повторюсь на счет ленивых вычислений. В Haskell что-то начинает вычисляться только тогда, когда оно действительно понадобится. Вы можете объявить список из 9000 элементов, но если вам понадобятся только один из них (и он не будет зависеть от других элементов списка), то будет вычислено значение только этого одного элемента. И этот принцип работает везде , а не только в списках. В каком-нибудь C++ такое тоже можно сделать, но придется самому написать соответствующий код или подключить какую-нибудь библиотеку, а в Haskell все уже есть «из коробки».

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

Рассмотрим другой пример. Допустим, у нас есть некая функция f(a, b) . Пусть аргументы a и b — результаты вычислений неких других функций, которые по счастливому стечению обстоятельств являются чистыми. В этом случае a и b могут быть вычислены параллельно! Согласитесь, в эпоху многоядерных процессоров это немаловажно. В ООП языках такую оптимизацию провести намного сложнее.

Кроме того, что мы получаем прирост производительности, мы также автоматически распараллеливаем программу и синхронизируем потоки выполнения! А ведь многопоточное программирование с его дэдлоками — это чуть ли не вторая по важности проблема современного программирования после ручного управления памятью. Если вы думаете, что описанное выше — лишь теория, посмотрите список расширений компилятора GHC и проект Data Parallel Haskell . Все уже реализовано!

Дополнение: См также проект Haskell STM . Транзакционная память интересна тем, что позволяет избавиться от блокировок в многопоточных приложениях.

К сожалению, мне не удалось найти ни одного бэнчмарка программ на Haskell, кроме shootout.alioth.debian.org . «К сожалению» — потому что у меня к этому бенчмарку много вопросов . Я отказываюсь верить, что программы на Pascal выполняются в 2 раза медленнее программ на Си. Также вызывают сомнения погрешности в стиле ±100% для скриптовых языков. Тем не менее, если верить этому бенчмарку, Haskell на сегодняшний день является самым быстрым языком функционального программирования. По скорости он сравним с языками Java и C#.

5. Меньше рефакторинга

Давайте откроем оглавление книги Рефакторинг — улучшение существующего кода Фаулера и посмотрим, какие типы рефакторинга бывают. Выделение метода, встраивание метода, перемещение метода, встраивание временной переменной, замена временной переменной вызовом метода, введение пояснительной переменной, расщепление временной переменной, замена ссылки значением, замена значения ссылкой… как считаете, многое ли из перечисленного применимо в Haskell при условии, что в этом языке нет ни ссылок, ни переменных (let и where не считаются)?

Недавно я провел на Хабре два опроса, один — в блоге «Java», второй — в блоге «Haskell». Вопрос был одним и тем же — «Как часто вам приходится производить рефакторинг кода на %PROGRAMMING_LANGUAGE%?». На этих опросах я слил почти всю свою карму (кому-то правда глаза режет?), но оно того стоило:

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

Что интересно, сам синтаксис Haskell подталкивает к написанию кода, состоящего из большого количества простых функций. Это связано с тем, что код на Haskell очень любит разрастаться в ширину экрана, а не в высоту, как в ООП языках. Кроме того, если внутри конструкции if-then-else требуется нечто большее, чем просто вернуть значение, то вместо if-then-else намного удобнее определить новую функцию. В действительности, благодаря сопоставлению с образцами и охране , на Haskell можно писать вообще без использования конструкции if-then-else.

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

6. А нужны ли TDD и UML?

Вспомним, чему нас учит любой учебник по Python или Java. Все является объектом. Используйте объекты, и ваш код станет намного легче сопровождать, расширять и повторно использовать. Уверуйте и покайтесь! Вот, правда, чтобы сопровождать код, извольте составить диаграммы классов, иначе в нем будет невозможно разобраться. И обязательно напишите тесты, покрывающие как можно бо льшую часть кода, иначе любое его изменение сделает вашу программу нерабочей. Прошу вопросы. Да, пожалуйста. Что-что, простите? В каком это смысле «всего лишь надстройка над процедурным программированием »?! Вы что, не слушали? Инкапсуляция, наследование, полиморфизм!

Нет, правда, вы никогда не задумывались, сколько нужно использовать костылей, чтобы ООП работало? Скачайте хотя бы уже упомянутый 253-ий выпуск Radio-T и послушайте, что там говорят о TDD . Фактически, нам предлагают делать в два, а то и в три раза (считая UML) больше работы!

Люди иногда спрашивают, «Что служит аналогом UML для Haskell?». Когда меня впервые спросили об этом 10 лет назад, я подумал, «Ума не приложу. Может быть, нам стоит придумать свой UML». Сейчас я думаю, «Это просто типы!». // Саймон Пейтон Джонс

При этом обычно умалчивается, что далеко не любая задача легко вписывается в рамки ООП. Фактически, ООП неплохо себя зарекомендовало только в игростроении, создании GUI и графических редакторов. Зайдите, к примеру, на CPAN . Возьмите 10 случайных модулей и посмотрите, сколько из них действительно используют ООП (да, в Perl есть ООП), а не просто оформляют модуль в виде класса. Или загляните на Hackage и посчитайте, сколько модулей вполне успешно решают практические задачи вообще без единого намека на ООП.

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

Почему мне кажется, что «костылей» должно быть меньше при использовании Haskell? Ну, во-первых, раз нет классов (не путать с классами типов ) — не нужно рисовать их диаграммы:) Теоретически ничто не мешает рисовать диаграммы классов типов, но лично мне такие еще ни разу не попадались. Во-вторых, как мы уже выяснили, Haskell — «очень декларативный» язык. То есть, обычно мы пишем на нем, что хотим получить, а не как . Таким образом, программа документирует сама себя. И в-третьих, строгая статическая типизация языка позволяет ликвидировать целые классы ошибок еще на этапе компиляции программы. Это не отменяет необходимость временами писать тесты, но существенно сокращает их количество.

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

Знаю, звучит нелепо. Сам не верил, пока не попробовал. Это выглядит примерно так. Создаем новый модуль. Вводим пару новых типов, объявляем первую функцию. Запускаем интерпретатор ghci, проверяем функцию на типичных данных и паре краевых случаев. Если работает — забываем про только что написанную функцию и пишем следующую. Когда весь необходимый функционал реализован, смотрим, какие сущности следует экспортировать из модуля и вносим соответствующие правки. Все, работа выполнена! Без единой компиляции. Если программа заработала в ghci, то ее правильно соберет любой компилятор Haskell, под любую платформу. Вот, что такое настоящая кроссплатформенность.

7. Широкая область применения

Распространенное заблуждение в отношении Haskell заключается в том, что этот язык якобы пригоден для решения только узкого круга хитроумных математических задач. И действительно, как прикажете решать «обычные» задачи, если у нас даже циклов нет? На самом деле, рекурсия ничем не хуже циклов. Думать итерациями или думать рекурсиями — это всего лишь дело привычки. И нет, рекурсия совсем не обязательно должна использовать какую-то память. Вообще-то , именно злоупотребление ООП в конечном счете приводит к мышлению по шаблону, и выводам в стиле «видимо, эта задачка с олимпиады по спортивному программированию , раз я не знаю, как ее решить».

Однако вернемся к области применения. Как я уже отмечал, Haskell прекрасно подходит для написания GUI приложений . Писать на нем для веб я еще не пробовал, но, судя по отзывам, Haskell и тут справляется не хуже PHP:

Я только что закончил написание простенькой FastCGI программы на Haskell. Мне хотелось понять, как работают веб-приложения на Haskell и подходит ли этот язык для создания сайта, посвященного изучению языков. Haskell не только справился с задачей. Оказалось, что писать на нем намного веселее, чем на PHP // jekor.com . На Haskell можно писать даже.

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

Надеюсь, заметка показалась вам интересной. Как обычно, я буду рад любым комментариям и вопросам читателей.

Дополнение: Книгу «Learn You a Haskell for Great Good!» в русском переводе уже можно заказать в интернет-магазинах . Также на просторах интернета была найдена годная книга Антона Холомьёва «Учебник по Haskell» . Если у вас «нет времени» на изучение Haskell, попробуйте ознакомиться с учебником Макеева Григория , в нем всего лишь 114 страниц.

Мэтью Гриффин

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

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

В первую очередь - данные

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

Когда я впервые увидел функции в Haskell, я задался вопросом: «Что представляют собой данные? Эта функция что-то берет на вход и выдает что-то на выходе?» А при работе с Python у меня возникал вопрос: «WHAT DOES THE CODE SAY?»

Структура данных Haskell сформировала качественно новый образ моего мышления, который я принес и в свою работу на Python. Мой код стал заметно лучше. И хотя очень часто мне казалось, что форма описанных мною данных менялась без причины, однако на деле все становилось ясно при небольшом изучении вопроса. Ограничения свободы в изменении данных также делают код менее сложным и более понятным.

Читаемость кода

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

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

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

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

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

Про читаемость я говорю серьезно

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

Комментарии. Они занимают верхнюю строчку в одной из глав нашей «книги».

Эта глава описывает то, как Томми пошел в магазин и купил утку.

chapter:: Tommy -> Market -> Duck

Функции из другой, уменьшенной функции, в общей картине сокращают код по максимуму.

Краткость. Вам не потребуется тонна кода для воплощения ваших идей.

Вставные символы

Я также хотел упомянуть о вставных функциях, которые распространены в языке Haskell. Вставные функции (или операторы) - это те функции, которые используют between для двух аргументов вместо before . Простым примером является «+».

В языке Haskell мы имеем несколько вставных символов, которые используются по умолчанию: $, <$>, <-, ->, др. Они могут вас немного смутить на начале пути.

Не переживайте! Как только вы научитесь применять их, вы поймете, как они полезны и просты. Я насчитал около 5 символов, которые я использую регулярно.

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

Нужно полностью обновить знания

При изучении Haskell вы будете попутно сталкиваться с новыми терминами, например, функтор или монада.

Вам может показаться, что эти слова сложно запомнить и выучить. Когда вы начинаете знакомиться с популярным языком программирования, многие термины вам понятны и знакомы из изученных вами языков.

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

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

Например, функтор.

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

Map (+1) -- results in

Так я дал название этому - мапа. Слово «мапа» является очень простым для запоминания. Список - это функтор. Список - это мапа.

Моя система проверки ошибок

Когда я писал на Python, моим инструментом отладки были операторы печати.

В Haskell я пользовался систематическими инструментами.

Но! Вы можете применить Debug.Trace . Данный прием схож с тем, как в Python функция печати не зависит от Haskell IO. И данный модуль может вначале принести пользу. Когда вы только начинали работать с Haskell, вы думали о том, как много вы будете его использовать?

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

Лучший учебник по монадам

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

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

Окей, сейчас я расскажу подробнее. Я буду объяснять на Haskell.

Я нашел видео на YouTube, «Parsing Stuff in Haskell », в котором описывалось, как сделать JSON анализ в Haskell, используя при этом библиотеку Parsec.

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

После написания синтаксического анализа при помощи видео, я начал понимать весь код. Я также начал понимать всю его «природу». Но на начальном этапе это не пригодится.

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

Польза от ваших знаний

Haskell является моим основным языком по нескольким причинам:

  1. Выбор технологий, которыми я буду пользоваться.
  2. Я могу писать свои программы быстрее, и чаще всего эти программы я и продаю.
  3. Не приходится иметь дело с мелкими багами.
  4. Даже сталкиваясь с несколькими ошибками, я быстро решаю их, так как они более-менее понятны.
  5. Python не делал акцент на скорости работы. Haskell делает то же самое, но выбор все же за мной.
  6. Рефакторинг, по сути, достаточно «ветреный». В Python я иногда сильно ругал себя, когда забывал править небольшие ошибки в коде, которые позже вызывали огромные затруднения.
  7. Замечательные библиотеки. Основная особенность Haskell заключается в высоком качестве библиотек.
  8. Сообщество , всегда готовое помочь.
  9. Простота масштабирования кода до нужного ядра.
  10. Haskell часто обновляется. В прошлом году, когда GHC (компилятор для Haskell) был обновлен до версии 7.8, делая при этом написание кода в два раза удобнее, были ускорены многие веб-сервера.

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

  • Tutorial

Привет Habr! Сегодня я достал свои старые конспекты по курсу «Haskell как первый язык программирования» Сергея Михайловича Абрамова и попробую максимально доходчиво и с примерами рассказать об этом замечательном языке тем, кто с ним еще не знаком. Рассказ ориентирован на неподготовленного читателя. Так что, даже если вы впервые услышали слово Haskell…

Базовые типы Haskell
Базовые типы языка Haskell - это:
Числа
Логические величины
Символы
Списки
Упорядоченные множества (tuples)
Функции

Числа
Целые:
Integer (-∞,∞)
Int (-2^31, 2^31-1)
В прелюдии (стандартной библиотеке) определенно много полезных функций для целых чисел, в том числе, и преобразование в число с плавающей точкой (fromInt и fromInteger)

Числа с плавающей точкой:
Float (7 знаков после запятой)
Double (16 знаков после запятой)

Логические величины
Bool (True | False)
Операции конъюнкции, дизъюнкции и отрицания (&&, ||, not)

Символы
Char (’a’)
И функции Char в Int и Int в Char (ord, chr)

Списки
Списки могут быть разные:
- список целых чисел
- список символов (строка)
[] - массив
- это список функций
и т. д.

Несколько стандартных операций в примерах:
Main> head
1
Main> tail
Main> length
2
Main> reverse
Main> 0:
Main> - строка приглашения в консоли компилятора ghci
«:» - операция присоединения элемента к списку.

Упорядоченные множества
Примеры:
(2.4, ”cat”) (Float, )
(’a’, True, 1) (Char, Bool, Int)
(,sqrt) (, Float->Float)
(1, (2, 3)) (Int, (Int, Int))

Но, сердце Haskell и всего функционального программирования - это, конечно, сами функции!

Функции
Функция, в современной математике, это закон соответствия, который сопоставляет каждому элементу x из данного множества A один единственный (или ни одного) элемент y из множества B.
Haskell, по своему назначению, это, прежде всего, язык математиков, поэтому синтаксис тут максимально точно соответствует этому определению.
Пример:
square:: Integer -> Integer square x = x*x
Как вы, наверное, догадались, это функция возведения числа в квадрат. Разберем её подробно:

Первая строка - это объявление функции:
Имя_функции:: область_определения - > область _значений
square:: Integer -> Integer
Тут следует сказать, что в Haskell совсем необязательно всегда объявлять функцию. В ряде случаев интерпретатор и так поймет какие у данной функции области определения и значения. Однако, опускать объявления - моветон.

Вторая строка - это определение функции:
Имя_функции параметры = правило_вычисления
square x = x*x

Функция без параметров есть ничто иное, как константа:
e:: Float e = exp 1.0

Функция с несколькими параметрами:
Спасибо janatem за разъяснения ().
abcFormula:: Float -> Float -> Float -> abcFormula a b c = [ (-b+sqrt(b*b-4.0*a*c))/(2.0*a), (-b-sqrt(b*b-4.0*a*c))/(2.0*a) ] -- находит корни уравнения ax^2+bx+c=0

Определения функций с альтернативами
Как и в любом языке, в Haskell есть конструкции ветвления.
Разберем их на примере функции abs (модуль).
If … then … else …
abs1 x = if x>=0 then x else -x

Case … of …
abs2 x = case x>=0 of True -> x False -> -x

Но, помимо стандартных if и case, в Haskell есть очень красивая и наиболее используемая конструкция ветвления. Так называемые, охранные выражения . Пример:
abs3 x | x>0 = x | x<0 = -x | otherwise = 0
Прямую черту следует читать, как: «при».
Читаем: «Функция abs3, с входным параметром x, при x>0 принимает значение x, при x<0 принимает значение -x, и в любом другом случае принимает значение 0».
Конечно, мы могли записать все с помощью двух охранных выражений, но я записал три, что бы было понятно, что их может быть сколько угодно.
Otherwise в прелюдии определен очень просто:
otherwise:: Bool otherwise = True
То есть, можно спокойно написать вместо «otherwise» «True», но это, опять же, моветон.

Сопоставление с образцом
Один из наиболее распространенных и эффективных приемов в Haskell - это сопоставление с образцом. Вместо параметра мы можем подсунуть функции пример того, как должен выглядеть параметр. Если образец подошел функция выполняется, если нет - переходит к следующему образцу. Например, определение факториала через рекурсию с помощью образцов:
fact:: Integer -> Integer fact 0 = 1 fact n = n * fact (n-1)
Тоже самое, но, с помощью охранных выражений:
fact:: Integer -> Integer fact n | n==0 = 1 | n>0 = n*fact (n-1)
Есть очень распространенный образец для списка: (x:xs). X - обозначает первый элемент, XS - остальной список (кроме первого элемента). «:» - операция присоединения элемента к списку. Примеры из прелюдии:
head:: [a] -> a head (x:_) = x head = error "Prelude.head: empty list" tail:: [a] -> [a] tail (_:xs) = xs tail = error "Prelude.tail: empty list"
Функция head принимает на вход список чего угодно [a] и возвращает первый элемент этого списка. Функция tail принимает на вход список чего угодно [a] и изымает из этого списка первый элемент.
«_» - означает, что данный элемент нас не интересует.

Ну вот, на сегодня и все. Если будет интерес, в ближайшее время напишу продолжение.

Используется рекурсивное определение факториала. Пример состоит из трех частей:

  • определение функции factorial , принимающей на вход один аргумент типа Integer (целое число неограниченной точности) и имеющей выход того же типа. Функция определяется рекурсивно, тип параметров задан в явном виде, чтобы избежать неоднозначности их определения.
  • определение функции line , которая выводит на печать число и его факториал в нужном формате. Использование команды printf аналогично языку C++ .
  • собственно вывод чисел и их факториалов. Для этого командой создается список чисел от 0 до 16, включительно. Функция двух аргументов map применяет первый аргумент (функцию line) к каждому элементу второго аргумента (списка ) и в результате создает список так называемых действий вывода (являющихся в Haskell обычными значениями). Для того, чтобы объединить эти действия в одно, используется команда sequence_ , которая, будучи применена к списку действий, выполняет первое действие из списка и затем рекурсивно применяет себя к хвосту списка.

Факториал:

Пример для версий GHC 6.6.1

Функция fac определена как произведение чисел от 1 до n через встроенную функцию языка product.

Hello, World!:

Пример для версий GHC 6.10.4

Числа Фибоначчи:

Пример для версий GHC 6.10.4

Этот пример использует одну из основных особенностей языка Haskell — возможность ленивых вычислений и использования бесконечных списков. Бесконечный список чисел Фибоначчи fibs определяется при помощи фунции zipWith , которая применяет первый аргумент (функцию двух переменных, в данном случае +) к парам соответствующих элементов второго и третьего аргументов (списков). tail fibs возвращает хвост списка fibs (т.е. все элементы, кроме первого). Таким образом первый элемент списка, возвращаемого zipWith , является суммой первого и второго элементов списка fibs и становится третьим его элементом.

Числа Фибоначчи:

Пример для версий GHC 6.10.4

Этот пример использует рекурсивное определение чисел Фибоначчи через пары соседних чисел в последовательности. На печать выводятся только первые элементы пар.

module Main where import Text.Printf fibNextPair :: (Int , Int ) -> (Int , Int ) fibNextPair (x , y ) = (y , x + y ) fibPair :: Int -> (Int , Int ) fibPair n | n == 1 = (1 , 1 ) | otherwise = fibNextPair (fibPair (n - 1 )) line n = printf "%d, " $ (fst . fibPair ) n main = do sequence_ $ map line [ 1 .. 16 ] putStrLn "..."

Квадратное уравнение:

Пример для версий GHC 6.10.4

Haskell предоставляет тип данных для работы с комплексными числами. Функция quadratic принимает в качестве аргумента список трех комплексных чисел (коэффициентов уравнения) и возвращает список корней уравнения. Запись вида root sign позволяет передать знак операции в качестве аргумента и таким образом обобщить запись двух знаков при квадратном корне из дискриминанта.

module Main where import Data.Complex import System.IO (hFlush , stdout ) quadratic :: (Complex Double , Complex Double , Complex Double ) -> [ Complex Double ] quadratic (0 , _ , _ ) = quadratic (a , b , c ) | d == 0 = [ root (+ )] | otherwise = [ root (+ ), root (- )] where d = b * b - 4 * a * c root sign = sign (- b ) (sqrt d ) / (2 * a ) main = do putStr "A = " hFlush stdout a <- readLn :: IO Double putStr "B = " hFlush stdout b <- readLn :: IO Double putStr "C = " hFlush stdout c <- readLn :: IO Double print $ quadratic (realToFrac a , realToFrac b , realToFrac c )



Теоретические основы императивного программирования были заложены ещё в 30-х годах XX века Аланом Тьюрингом и Джоном фон Нейманом. Теория, положенная в основу функционального подхода, формировалась в 20-х и 30-х годах. В числе разработчиков математических основ функционального программирования - Мозес Шёнфинкель (Германия и Россия) и Хаскелл Карри (Англия), а также Алонзо Чёрч (США). Шёнфинкель и Карри заложили основы комбинаторной логики, а Чёрч является создателем лямбда-исчисления.

Функциональное программирование как раз основано на идеях из комбинаторной логики и лямбда-исчисления.

Но теория так и оставалась теорией, пока в начале 50-х прошлого века Джон МакКарти не разработал язык Lisp (1958), который стал первым почти функциональным языком программирования. На протяжении многих лет у Lisp не было конкурентов. Позднее появились функциональные языки программирования APL (1964), ISWIM (1966) и FP (1977), которые не получили столь широкого распространения.

Со временем Lisp перестал удовлетворять некоторым требованиям разработчиков программ, особенно с ростом объема и сложности программного кода. В связи с этим обстоятельством всё большую роль стала играть типизация. В конце 70-х - начале 80-х годов XX века интенсивно разрабатывались модели типизации, подходящие для функциональных языков.

Большинство этих моделей включали в себя поддержку таких мощных механизмов как абстракция данных и полиморфизм. Появилось множество типизированных функциональных языков: ML, Scheme, Hope, Miranda, Clean и многие другие. Вдобавок постоянно увеличивалось число диалектов.

ML (1973) – первый язык с типизацией Хиндли–Милнера;
Scheme (1975) - это один из двух самых популярных диалектов языка Lisp;
SASL, KRC, Miranda (1972–1985) – одни из первых ленивых языков;
Hope (1980) – один из первых языков с алгебраическими типами данных.


Хаскелл Карри

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

История языка Haskell начинается в 1987 году. Один за другим появлялись новые функциональные языки программирования. После выхода Miranda (Research Software Ltd, 1985 год) возрос интерес к ленивым вычислениям: к 1987 году возникло более дюжины нестрогих чисто функциональных языков программирования.

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

Так появился Haskell. Он был назван в честь одного из основателей комбинаторной логики Хаскела Кaрри (Haskell Curry).

К концу 1980-х годов было создано много функциональных языков. Часть из них оказали значительное влияние на Haskell:

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

Haskell 1.0 - 1.4

Первая версия Haskell (Haskell 1.0) была выпущена в 1990г. Попытки комитета вылились в серию реализаций языка (1.0, 1.1, 1.2, 1.3, 1.4).

Haskell 98

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

В феврале 1999 года стандарт языка Haskell 98 впервые был опубликован, как «The Haskell 98 Report». В январе 2003 года измененная версия была опубликована как «Haskell 98 Language and Libraries: The Revised Report». Язык продолжил стремительно развиваться, реализация компилятора Glasgow Haskell Compiler (GHC) представляет фактический стандарт языка.

Haskell 2010

Современный стандарт Haskell - Haskell 2010 - был анонсирован 24 ноября 2009 года; GHC поддерживает его с версии 7.0.1.

По сравнению с Haskell "98 он содержал следующие изменения:

Do и If Then Else
Иерархические модули
Объявления пустых переменных
Решение устойчивости
Интерфейс сторонних функций
Линейный синтаксис комментариев
Охраняющие паттерны
Облегченных анализ зависимостей
Указания для языка (pragma)
Отсутствие паттернов n+k

Отсутствие контекста типов данных
Маркированные списки переменных

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

От создателя Haskell

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

Более того, язык Curry предоставляет дополнительные механизмы по сравнению с чистыми языками программирования (по сравнению с функциональными языками - поиск и вычисления по неполным данным, по сравнению с логическими языками - более эффективный механизм вычислений благодаря детерминизму и вызову по необходимости для функций).

Популярность

На сайте Github сейчас занимает 23-ю строчку по популярности среди языков программирования.

Интерпретатор/компилятор Pugs для языка Perl 6.

Язык описания аппаратных средств Lava .

Система обработки натурального языка LOLITA.

Системы доказательства теорем Equinox / Paradox и Agda .

Facebook

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

Несмотря на молодость, экспериментальный статус и относительно низкую популярность, Facebook выбрал именно Haskell для создания важного модуля. Один из инженеров Луис Брэнди (Louis Brandy), который входит в группу разработчиков нового антиспамерского фильтра, провел два года за этим проектом вместе с коллегами. В интервью Wired он объяснил, чем они руководствовались.

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

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

Тем не менее, индустрия точно двигается в нужном направлении, как показывает пример новых языков программирования, ориентированных на выполнение параллельных процессов, например, Go от Google и Rust от Mozilla. Пусть они не такие эффективные, как Haskell, зато проще в изучении. В любом случае, Haskell можно поблагодарить за то, что он подтолкнул к развитию другие языки программирования и способствовал запуску новых перспективных проектов.

Евгений Козлов в своем блоге рассказал о впечатлениях от работы с Haskell:

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

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

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

Если написан код, то его сложно интерпретировать двумя способами. Например, нельзя перепутать применение функции и ссылку на функцию, как в Scala, так как в Haskell функции являются объектами первого класса. Или, например, нельзя перепутать функцию с типом или классом, так как все функции должны начинаться с маленькой буквы, а типы/классы – с большой.

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



Есть вопросы?

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: