Haskell язык программирования. Функции в Haskell

Детские товары 14.04.2019

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

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

В основе функционального программирования лежит простое представление о том, что функции в языке ничем не отличаются от переменных. Обычно к этому добавляют, что функции в целом должны быть функциями в математическом смысле, то есть не должны иметь некого внутреннего состояния и не должны неявно изменять внешнего состояния (окружения). Прямым следствием отсутствия состояния является тот факт, что функция, вызванная с одними и теми же аргументами возвращает одно и то же значение. Следствием неизменности внешнего состояния является отсутствие побочных эффектов. Например, printf в C/C++ принимает некие аргументы, и возвращает число. Но кроме этого, printf выводит символы в терминал. Последнее является побочным эффектом. В общем случае, неявные изменения внешнего (по отношнению к программе) состояния считаются побочными эффектами.

Но если внешнее состояние неизменно, программа неизбежно превращается в некую Кантовскую “вещь в себе” – она не может читать/записывать файлы, терминал, и т.д. Это не выглядит очень полезным. На самом деле, ФЯП выходят из положения, вводя некую псевдопеременную “мир”, которая может явно быть передана главной функции программы (main), и ее измененное состояние явно возвращается главной функцией программы. Таким образом, функциональную программу можно алгебраически представить как некую функцию:

\[ W_{n+1} = P(W_{n}), \]

где \(W\) – состояние окружения (“мир”), а \(P\) – это программа.

Другая особенность ФЯП, прямо следующая из того, что функции не имеют и не изменяют состояния заключается в том, что в ФЯП очень часто нет “переменных”. Любые выражения являются константами, если смотреть с точки зрения императивных языков. Это свойство называется “ссылочной прозрачностью”: если вы видите, что функция принимает переменную, вы всегда точно знаете, какое значение имеет эта переменная.

Подобный подход значительно упрощает “думание” о программах: больше не нужно держать в голове состояние – код работает ровно так, как читается – никаких скрытых параметров.

Использование чистых функций, помимо прочего, позволяет значительно упростить реализацию функций высших порядков, т.е. функций, оперирующих с функциями (которые тоже могут оперировать с функциями, и так далее). Поэтому, большинство ФЯП имеют поддержку функций высшего порядка “встроенными” в язык интуитивным образом. В качестве небольшого примера, приведу сравнение реализаций функции for_each , работающей со списком на C++ и Haskell.

Функции высшего порядка

На Haskell аналогичный код будет выглядеть так:

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

На самом деле в языке уже есть такая функция, и она называется map . Вернемся к ней чуть позже.

Каррирование

Очень интересным мометном здесь является использование (1+) для добавления 1. Для этого придется рассказать про нотацию Хаскеля Брукса Карри (американского математика, 1900-1982), в честь которого назван язык. Эта нотация называется частичной аппликацией функций или каррирование .

Вкратце, любая функция нескольких аргументов \(f(x,y)\) может быть разложена на последовательное применение нескольких функций одного аргумента. Например, \(f(2,3)\) может быть разложена как:

\[ g(y) = f(2,y) \] \[ f(2,3) = h(g(3)) \]

Можно заметить, что функция двух аргументов \(f(x,y)\) превращается в функцию одного аргумента \(g(y)=f(x,y)\) . Это и есть каррирование. Если записать то же самое в нотации Haskell:

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

Какое отношение это имеет к (1+) ? Самое прямое. (1+) . Сложение является функцией двух аргументов и может быть записано как x+y = (+) x y . Тогда применение к одному аргументу (+) 1 даст нам функцию одного аргумента, которая прибавляет к аргументу 1. (1+) – это просто синтаксическое соглашение (“сахар”), которое сводится к (+) 1 .

Порядок применения функции

Чтобы каррирование работало, аргументы функции применяются слева направо (или лево-ассоциативно). Таким образом, выражение вида

Означает то же самое, что

Это отвечает на вопрос, почему нельзя записать print for_each (1+) и ожидать адекватных результатов (на самом деле такой код просто не скомпилируется, поскольку print for_each не может принимать аргументы)

Существует оператор $ , который является право -ассоциативным применением функции. Поэтому код можно записать так:

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

Эта-редукция, композиция функций и pointfree-нотация

Другой способ экономии – это эта-редкукция.

Эта-редукция позволяет писать достаточно компактный и при этом вполне понятный код, опуская “лишние” переменные. Такая нотация называется “pointfree” (аргументы иногда называют “точками”, отсюда такое название). Напишем программу, которая читает строки и выводит их в обратном порядке.

Пока не обращая внимания на последнюю строчку (считаем что там магия), рассмотрим функцию revlines. Очевидно, что x здесь вполне избыточен. В математике есть нотация композиции функций, обозначаемая точкой:

\[ f(g(x)) = (f\cdot g)(x) \]

Haskell имеет аналогичную нотацию: f (g x) = (f . g) x . Тогда мы можем переписать revlines в виде

Или, применяя эта-редукцию:

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

Аннотации типов

Можно заметить, что во всем до сих пор написанном коде нигде не указаны типы. Haskell является строго типизированным языком, но типы указывать не обязательно. Это потому, что Haskell выводит типы автоматически. Тем не менее, для повышения читаемости кода (и для уменьшения вероятности ошибки), типы можно указывать . Запишем типы некоторых функций, и обсудим что это значит:

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

Оператор -> – право -ассоциативен (ввиду каррирования), поэтому

т.е. “функция принимает два аргумента”, эквивалентно

т.е. “функция принимает один аргумент и возвращает функцию одного аргумента”

map как функция высшего порядка

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

Получаем чудовищное, по виду, сообщение об ошибке

Couldn"t match type ‘Char’ with ‘’ Expected type: -> Actual type: -> In the first argument of ‘withLines’, namely ‘indent’ In the expression: withLines indent

Почему это происходит? Давайте запишем типы всех функций.

Ошибка становится вполне очевидна: withLines ожидает функцию типа -> , а мы ему даем String->String . Вообще, String определен как , т.е. список символов. Это объясняет сообщение компилятора.

Вспомним про функцию map , которая производит операцию над каждым элементом массива. Ее тип

Подставляя a,b=String , обнаруживаем то, что ищем:

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

Секционирование операторов

Еще один пример использования функций высшего порядка

Здесь есть достаточно странный синтаксис. Для начала будем иметь ввиду, что любой бинарный оператор (а вообще говоря любая бинарная функция) может быть частично применен как

Вообще, `op` – это использование бинарной функции op как бинарного оператора

C тем же успехом можно частично применить второй аргумент:

Этот синтаксический сахар называется “секционирование операторов”

. – это бинарная функция высшего порядка:

Соответственно, если мы перепишем, скажем, withLines с аргументом, то получим:

Типы данных

В примерах раннее мы использовали тип данных списка (массива). Как этот тип устроен? Давайте попробуем сделать пользовательский тип списка.

Теперь мы можем записать что-то такое, например:

В Haskell, естественно, есть встроенный тип списка. Он определен следующим образом:

Можно просмотреть параллели. : – это Добавить, или cons , – это Пусто, или nil , а Список а – это [a] . Мы можем легко определить собственные операторы:

Или использовать их сразу в определении типа.

Единственное, что встроено в язык – это синтаксис вида , который автоматически преобразуется в x:y:...: .

Здесь Добавить и Пусто называются конструкторами типа Список . Это функции, которые “конструируют” значение типа Список. Не имеют практически ничего общего с конструкторами объектов в C++/Java.

Несколько примеров с нашим пользовательским типом:

С mystery1 , mystery2 все должно быть понятно. mystery3 – это бесконечный список, и Haskell все устраивает (поскольку это ленивый язык – значения вычисляются только при использовании), mystery4 – это ошибка типа. Int и String не могут находиться в одном списке.

Аналогично пишутся прочие типы данных.

Шаблоны аргументов

Для простоты, переопределим наш тип списка в латинице и сразу используем оператор:

Напишем несколько функций для нашего пользовательского списка:

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

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

Если мы не указываем все возможные варианты, то компилятор сообщает об этом, например:

Приводит к сообщению

Pattern match(es) are non-exhaustive In an equation for ‘isOne’: Patterns not matched: Nil

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

Тип Maybe

Немного странно кажется, что наша функция takeOne возвращает список, а не значение. Аналогичная библиотечная функция head возвращает значение, однако на пустом списке кидает исключение (да, в Haskell есть исключения, но их стараются избегать по очевидным причинам). Ее можно было бы записать в виде

undefined – ключевое слово, которое подходит под любой тип и аварийно завершает программу при попытке вычисления. Другой вариант undefined – это error x , где x имеет вид String . error выведет строку x перед завершением.

На помощь приходит тип Maybe . Он определен так:

В целом похоже на список, только без списка. takeOne можно переписать в виде:

Выглядит несколько более осмыслено, чем вариант со списком и гораздо более безопасно, чем undefined .

Попробуем написать поиск символа в строке с использованием типа Maybe:

Pattern guards

Предыдущий код можно переписать несколько короче:

Этот синтаксис называется “pattern guards”, или ограничители шаблонов в вольном переводе на русский. По сути, если выражение после | вычисляется как True , то шаблон срабатывает. Если нет – то проверяется следующий ограничитель, или следующий шаблон, если это был последний. otherwise – это просто более читаемый синоним для True .

В ограничителях так же возможно производить сравнения шаблонов. Синтаксис для этого:

Тогда код выше можно переписать в виде:

Классы типов

На самом деле мы можем записать более общий вариант поиска элемента в списке , практически ничего не делая:

Мы изменили только сигнатуру типа, остальное осталось прежним. Сигнатура выглядит странновато: в ней появилось выражение Eq a => . Оно означает, что тип a принадлежит классу Eq – классу типов, для которых определена эквивалентность, т.е. опреатор (==) .

Eq определен следующим образом:

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

Каким конкретно образом реализовано сравнение символов – довольно долгая история. Примитивные типы типа Int , Char или скажем Double достаточно глубоко встроены в язык. Мы, однако, можем определить экземпляры для более сложных типов:

Здесь мы опять вводим ограничение на класс. По сути мы утверждаем, что для всех типов a в классе Eq существует тип Maybe a , так же в классе Eq . Ничто не мешает добавлять собственные типы.

Классы типов позволяют писать обобщенные, и при этом типобезопасные функции.

Опять про Maybe

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

Это гораздо лучше, чем возвращать NULL , как в C/C++ . Во-первых, глядя на сигнатуру типа, сразу становится ясно, чего можно ожидать от функции. Во-вторых, компилятор не позволит производить вычисления с несуществующим значением. Если в “классических” языках типы в основном служат компилятору, то в функциональных типы служат в первую очередь программисту.

Очевидный вопрос с Maybe заключается в следующем: допустим у нас есть функция

Рассмотрим такой код:

Каков результат такого кода? Правильный ответ – ошибка типа. 1 имеет тип Int , в то время как takeOne возвращает тип Maybe Int . Проблема очевидна. Наивное решение этой проблемы в том, чтобы написать условие:

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

Теперь давайте вспомним, что, когда я говорил про map , я сказал, что эта функция “поднимает” свой первый аргумент в категорию списков. Эта же концепция применима ко всем производным типам. В языке сущесвтует обобщенная функция fmap . Посмотрим, как она работает:

Это выражение возвращает Just 2 . fmap “поднимает” свой аргумент в категорию производных типов. Каких именно – компилятор выводит автоматически из контекста.

Тип fmap определен как

Здесь используется класс Functor , единственное требование которого – чтобы был определен fmap . Для Maybe экземпляр определяется тривиально:

В общем случае класс Functor – это класс типов, принимающих один аргумент типа. Другой пример функтора – список. Таким образом, map является всего лишь частным случаем fmap .

Монады

Пока скажу только, что Maybe , и список являются монадами. Понятие монады происходит из теории категорий. Это понятие в большинстве функциональных языков является ключевым, поскольку, например, Haskell, инкапсулирует состояние окружения (то, которое функции не изменяют) в монаде IO (ввод-вывод). Наконец мы можем записать сигнатуру для main:

Функция main возвращает “пустоту” (на самом деле читается как unit, и аналогично по смыслу void в С) в монаде IO . IO , в свою очередь, где-то “внутри” содержит информацию о состоянии окружения.

Сточка, которая часто нам встречалась

использует следующие функции:

return и (>>=) (читается как bind) определены в классе Monad . return просто “поднимает” значение в монаду, а bind берет значение в монаде и передает его в функцию, возвращающую монаду. При этом некое “состояние”, инкапсулированное монадой (если оно есть), может неявно передаваться из функции в функцию через оператор bind.

Для интересующихся, хорошее объяснение:

Если есть интерес, выделим лекцию на продолжение.

Несмотря на то, что он не соответствует одному из ваших больших критериев (статический * ввод), я собираюсь сделать дело для Python. Вот несколько причин, по которым я думаю, вам стоит взглянуть на это:

  • Для императивного языка это удивительно функционально. Это было одно из того, что меня поразило, когда я узнал об этом. Например, обратите внимание на списки . Он имеет лямбды, первоклассные функции и множество функционально вдохновленных композиций на итераторах (карты, складки, молнии...). Это дает вам возможность выбрать любую парадигму, которая лучше всего подходит для этой проблемы.
  • ИМХО, это, как и Haskell, красиво кодировать. Синтаксис прост и элегантен.
  • У этого есть культура, которая сосредотачивается на том, чтобы делать вещи прямолинейно, вместо того, чтобы сосредотачиваться слишком аккуратно на эффективности.

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

* Я предполагаю, что вы подразумеваете статическую типизацию здесь, так как вы хотите объявить типы. Techincally, Python - это строго типизированный язык, поскольку вы не можете произвольно интерпретировать, скажем, строку как число. Интересно, что существуют производные Python, которые позволяют статическую типизацию, например Boo .

2018-12-04T00:00Z

Если вам нужен сильный (эр) личный пролог, Mercury - интересный выбор. В прошлом я занимался этим, и мне нравилась другая перспектива, которую он мне дал. Он также имеет moded-ness (какие параметры должны быть свободными / фиксированными) и детерминизм (сколько результатов есть?) В системе типов.

Clean очень похожа на Haskell, но имеет уникальную типизацию, которая используется как альтернатива Monads (более конкретно, монада IO). Уникальность ввода также делает интересный материал для работы с массивами.

2018-12-11T00:00Z

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

  • Липский диалект для кода-данных-данных / гомоконичности и потому что они являются хорошими, если не лучшими, примерами динамических (более или менее строгих) языков функционального программирования
  • Пролог как преобладающий логический язык программирования
  • Smalltalk как единственный истинный язык ООП (также интересный из-за его обычно чрезвычайно ориентированного на изображение подхода)
  • возможно, Erlang или Clojure, если вас интересуют языки, выкованные для параллельного / параллельного / распределенного программирования
  • Форвард для программирования, ориентированного на стек
  • (Haskell для строгого функционального статически типизированного ленивого программирования)

Особенно Lisps (CL не так много, как Scheme) и Prolog (и Haskell) охватывают рекурсию.

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

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

2018-12-18T00:00Z

С точки зрения того, что подходит вашему майору, очевидный выбор кажется логическим языком, таким как Prolog или его производные. Логическое программирование может быть сделано очень аккуратно на функциональном языке (см., Например, The Reasoned Schemer), но вам может понравиться работать с логической парадигмой напрямую.

Интерактивная теоретическая система доказательства, такая как twelf или coq, может также поразить вашу фантазию.

2018-12-25T00:00Z

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

Сильная система типов. (...) Он также делает неформальные рассуждения о правильности моей программы более легкими. Меня беспокоит правильность, а не эффективность.

Акцент на рекурсии, а не на итерации. (...)

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

    Не обращайте внимания на практичность и отправляйтесь на «больше Haskell, чем Haskell» : система типа Haskell полна дыр, из-за прекращения и других беспорядочных компромиссов. Очистите беспорядок и добавьте более мощные функции, и вы получите такие языки, как Coq и Agda , где тип функции содержит доказательство ее правильности (вы можете даже читать функцию arrow -> как логическую импликацию!). Эти языки использовались для математических доказательств и для программ с чрезвычайно высокими требованиями к правильности. Coq, вероятно, является самым выдающимся языком стиля, но у Agda больше чувство Haskell-y (а также написано в Haskell).

    Не учитывайте типы, добавляйте больше магии : если Haskell - колдовство, Lisp - это сырая первобытная магия творения. Языки семейства Lisp (включая Scheme and Clojure ) обладают почти беспрецедентной гибкостью в сочетании с экстремальным минимализмом. Языки по существу не имеют синтаксиса, написание кода непосредственно в виде структуры данных дерева; метапрограммирование в Lisp проще, чем не-мета-программирование на некоторых языках.

    Компромисс немного и приближайтесь к мейнстриму : Haskell попадает в широкую семью языков, сильно влияющих на ML, любой из которых вы, вероятно, могли бы перейти без особых трудностей. Haskell является одним из самых строгих, когда речь заходит о гарантиях правильности от типов и использования функционального стиля, где другие часто являются либо гибридными стилями, либо / или делают прагматические компромиссы по разным причинам. Если вы хотите подвергнуть ООП и доступ к множеству основных технологических платформ, либо Scala на JVM, либо F # на.NET имеет много общего с Haskell, обеспечивая легкую совместимость с платформами Java и.NET. F # поддерживается непосредственно Microsoft, но имеет некоторые досадные ограничения по сравнению с Haskell и проблемами переносимости на платформах, отличных от Windows. У Scala есть прямые аналоги с системой типов Haskell и кросс-платформенным потенциалом Java, но она имеет более тяжелый синтаксис и не обладает мощной поддержкой сторонних разработчиков, которой пользуется F #.

Рефакторинге, 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 страниц.

В 1985 году был разработан первый полностью функциональный язык, названный Мирандой (предшественником его был Lisp, который нельзя считать чисто функциональным). Он был очень популярен в среде программистов, однако, к сожалению, принадлежал только одной компании. Поэтому его дальнейшее развитие было сильно ограничено, что привело к возникновению множества похожих на него независимых языков. В 1987 в Орегоне был собран комитет, состоящий из энтузиастов функционального программирования, который постановил, что им необходим единый язык, который бы сосредоточил в себе лучшие достижения ФП за последние годы. В результате этого в 1990 году вышла первая версия, названная по имени Хаскелла Карри, известного математика. В 1999 был опубликован стандарт языка, а компилятор GHC, разработанный в университете Глазго стал самой известной реализацией языка (кстати, GHC, примерно на 80 % написан на самом хаскелле).

Философия языка Haskell

Haskell, мягко говоря, не похож на большинство своих объектно-ориентированных собратьев. Например, здесь нет привычных операторов присваивания значения переменной (вообще, переменных тоже нет) или циклов (вместо циклов используется рекурсия). А ещё, он с самого начала поддерживал такие вещи, как лямбда-исчисления, нестрогую семантику, монадическую систему. Некоторые элементы из этого (лямбда-выражения, которые происходят от лямбда-исчислений) только недавно внедрили в такие языки, как Java, Python или JavaScript.

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

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

Где применяется Haskell

Haskell некоторое время назад был известен только в сфере фанатов математики и функционального программирования, однако, вопреки слухам, это уже давно не так. Сейчас его используют в Facebook для фильтров спама и он успешно справляется со своей задачей. Возможности Хаскель изучали специалисты в Microsoft Research, в результате чего появилась урезанная версии языка, названная F#, которая сейчас доступна в Visual Studio.

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

Haskell также хорошо подходит для создания оконных приложений (GUI). Можно найти сделанные на Haskell текстовые редакторы, оконные менеджеры, торрент-клиенты, игры (шутеры и логические), браузеры, движки для рендеринга 3D и.т.д. Уже существуют веб-фреймворки на нем, а также приложения для работы с базами данных. Также известен инструмент для криптографии под названии Cryptol и система управления версиями Darcs.

Сложность обучения Haskell

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

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

Плюсы/минусы Haskell

Плюсы Хаскелля - это его красота и лаконичность. Программы на Haskell проще читать и понимать (даже не зная некоторых деталей языка). Его синтаксис защищает программиста от типичных ошибок. На нем проще спроектировать и создать даже большие сложные программы. И он лишен многих типичных проблемы других языков.

Из минусов:

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

Поэтому, на текущий момент, будет трудно найти вакансию программиста на этом языке (разве что, в некоторых лабораториях, разрабатывающих искусственный интеллект).

Сопутствующие технологии

GHC - самый популярный, на сегодняшний день, компилятор для хаскелля.

Snap, Yesod, Happstack - фреймворки, предназначенные для веб-разработки.

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

Bluespec SystemVerilog - расширение для хаскелля, применяется для проектирования полупроводниковых схем.

Мне задавали их множество раз. Отвечаю.

«Что такое этот ваш Haskell?»

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

«Это что, какой-то новый язык?»

Вовсе нет. История Haskell началась ещё в 1987 году. Этот язык был рождён в математических кругах, когда группа людей решила создать лучший функциональный язык программирования. В 1990 году вышла первая версия языка, названного в честь известного американского математика Хаскелла Карри . В 1998 году язык был стандартизован, а начиная с 2000-х началось его медленное вхождение в мир практического программирования. За эти годы язык совершенствовался, и вот в 2010 мир увидел его обновлённый стандарт. Так что мы имеем дело с языком, который старше Java.

«И кто его сделал?»

Haskell создавался многими людьми. Наиболее известная реализация языка нашла своё воплощение в компиляторе GHC (The Glasgow Haskell Compiler), родившегося в 1989 году в Университете Глазго. У компилятора было несколько главных разработчиков, из которых наиболее известны двое, Simon Peyton Jones и Simon Marlow . Впоследствии весомый вклад в разработку GHC внесли ещё несколько сотен человек. Исходный код компилятора GHC открыт . Кстати, сам компилятор на 82% написан на Haskell.

Для любопытных: исчерпывающее повествование об истории Haskell и GHC читайте .

«А библиотеки для Haskell имеются?»

О да! Их даже не сотни - их тысячи. В процессе чтения вы познакомитесь со многими из них.

«И что, его уже можно в production?»

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

«А порог вхождения в Haskell высокий?»

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

«А я слышал ещё про какие-то монады…»

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

«А если сравнить его с C++/Python/Scala…»

Сравнение Haskell с другими языками выходит за рамки этой книги. Несколько раз вы встретите здесь кусочки кода на других языках, но я привожу их исключительно для того, чтобы подчеркнуть различие с Haskell, а вовсе не для сравнения в контексте «лучше/хуже». И вообще, я буду изо всех сил стараться не восхвалять Haskell без меры, я хочу лишь рассказать вам правду о нём. Мой вывод об этом языке я уже сделал, а свой вывод о нём вы должны сделать сами.



Рекомендуем почитать

Наверх