Для чего используют haskell
Конспекты лекций «Haskell как первый язык программирования». Часть1
Привет 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,2,3]
1
Main> tail [1,2,3]
[2,3]
Main> length [True,False]
2
Main> reverse [1,2,3]
[3,2,1]
Main> 0:[1,2,3]
[0,1,2,3]
Main> — строка приглашения в консоли компилятора ghci
«:» — операция присоединения элемента к списку.
Упорядоченные множества
Примеры:
(2.4, ”cat”) (Float, [Char])
(’a’, True, 1) (Char, Bool, Int)
([1,2],sqrt) ([Int], Float->Float)
(1, (2, 3)) (Int, (Int, Int))
Но, сердце Haskell и всего функционального программирования — это, конечно, сами функции!
Функции
Функция, в современной математике, это закон соответствия, который сопоставляет каждому элементу x из данного множества A один единственный (или ни одного) элемент y из множества B.
Haskell, по своему назначению, это, прежде всего, язык математиков, поэтому синтаксис тут максимально точно соответствует этому определению.
Пример:
Как вы, наверное, догадались, это функция возведения числа в квадрат. Разберем её подробно:
Вторая строка — это определение функции:
Имя_функции параметры = правило_вычисления
square x = x*x
Функция без параметров есть ничто иное, как константа:
Функция с несколькими параметрами:
Спасибо janatem за разъяснения (читай комментарии).
Определения функций с альтернативами
Как и в любом языке, в Haskell есть конструкции ветвления.
Разберем их на примере функции abs (модуль).
If … then … else …
Но, помимо стандартных if и case, в Haskell есть очень красивая и наиболее используемая конструкция ветвления. Так называемые, охранные выражения. Пример:
Прямую черту следует читать, как: «при».
Читаем: «Функция abs3, с входным параметром x, при x>0 принимает значение x, при x
Haskell – хороший выбор с точки зрения безопасности ПО?
Команда Typeable понимает ценность безопасности. Мы любим Haskell, но стоит ли его выбирать, если ваша цель – создание защищенного программного обеспечения? Хотелось бы сказать «да», но как и для большинства эмпирических вопросов о разработке ПО, здесь просто нет объективного доказательства, подтверждающего, что Haskell – или ещё какой-нибудь язык программирования – обеспечивает большую безопасность, чем любой другой. Нельзя сказать, что выбор языка в Typeable не имеет значения для безопасности, но какое именно значение он имеет, еще нужно подумать.
Опираясь на пятилетний опыт преподавания вводного курса по защите ПО, я могу подтвердить, что универсальной теории, на которой может строиться защита ПО, не существует. Вопросы безопасности чаще всего преподаются путем перечисления различных уязвимостей, способов их минимизации и моделей безопасности в надежде на то, что на этой основе студенты смогут получить общее понимание вопроса. Из немногих существующих теоретических работ только некоторые содержат попытки провести связь между языком программирования и аспектами безопасности.
В этой статье я собираюсь коротко описать свою любимую точку зрения на то, как выбор языка программирования можно привязать к безопасности. Этот подход подразумевает распределение различных уязвимостей по шкале от «уязвимостей предметной области» до «побочных уязвимостей».
На оси выше показан источник различных уязвимостей программного обеспечения. На крайнем правом конце мы видим ошибки, связанные исключительно с доменной областью, то есть ошибки, совершенно не зависящие от используемого инструментария. Примером такой ошибки являются «контрольные вопросы», которые в начале 2000-х использовались многими веб-сервисами для восстановления паролей. Зачастую это были вопросы типа «Девичья фамилия вашей матери?». Позднее, примерно в 2009-2010 годах, возникло такое явление, как социальные сети, и неожиданно «девичья фамилия матери» перешла в категорию общедоступной информации. Неважно, какую технологию вы используете для реализации такой схемы с «контрольными вопросами». Эта схема все равно не работает.
На крайнем левом конце шкалы у нас представлены ошибки, имеющие чисто техническую причину. Они абсолютно не зависят от области задачи. Хорошим примером такой проблемы является печально известное переполнение буфера. Совершенно неважно, что хранится в буфере – если буфер переполнился, злоумышленник получает возможность беспрепятственно вмешаться в работу поддерживающих структур вашей программы. В данной ситуации переполнения буфера можно избежать, по крайней мере, теоретически, используя набор инструментальных средств, в котором отсутствует неконтролируемая запись в буфер.
Между этими крайними точками шкалы находится уязвимость, не вызванная ни чисто технической причиной, ни проблемами домена. Стандартный пример такой уязвимости обычно можно найти в сервисах, обеспечивающих выгрузку файлов.
Эта опасность сочетает в себе технический и доменный аспекты уязвимости. Маловероятно, что какой-нибудь набор инструментальных средств сможет предложить готовое решение для этой проблемы, однако легко увидеть, что одни инструменты могли бы обеспечить лучший контроль такого проблемного поведения, чем другие.
Использование шкалы
Польза этой шкалы заключается в оценке вашего инструментария, в том числе языка программирования и платформ. Сколько чисто технических проблем ваш инструментарий решает самостоятельно? Какую часть шкалы он закрывает, предлагая дополнительные возможности для устранения ошибок, которые приводят к уязвимостям?
В идеале современный инструментарий должен практически полностью устранять чисто технические уязвимости. Например, большинство современных языков, таких как Haskell, C# и Java, по большей части обеспечивают защиту содержимого памяти и в целом предотвращают переполнение буфера, попытки дважды освободить одну и ту же ячейку, а также другие технические проблемы. Однако от правильного инструментария можно получить еще больше пользы. Например, легко представить себе систему, в которой имеется техническая возможность разделить абсолютный и относительный пути к файлу, что упрощает контроль атак с обходом каталога (path traversal), таких как загрузка пользователем файла поверх какого-нибудь важного конфигурационного файла системы.
Haskell – нижняя часть шкалы
Как и большинство современных языков, Haskell хорошо работает с техническими уязвимостями низкого уровня. Во-первых, Haskell обеспечивает безопасный доступ к памяти, что делает огромный пласт возможных уязвимостей, особенно связанных с массивами данных и переполнением буфера, недосягаемым для потенциальных злоумышленников. Во-вторых, Haskell имеет статическую типизацию, что также защищает от целого семейства ошибок, например, от известных манипуляций с типами в PHP:
Аналогичная проблема возникла с Java (и другим языками, см. https://frohoff.github.io/appseccali-marshalling-pickles/). Java предложил исключительно удобный способ сериализации любого объекта на диск и восстановления этого объекта в исходной форме. Единственной досадной проблемой стало отсутствие способа сказать, какой объект вы ждете! Это позволяет злоумышленникам пересылать вам объекты, которые – после десериализации в вашей программе – превращаются во вредоносный код, сеющий разрушения и крадущий данные.
Это не значит, что вы не можете создать безопасный код на PHP или не можете получить такие же ошибки в Haskell, однако по своей природе Haskell не склонен к таким уязвимостям. Если переложить приведенный выше пример кода на Haskell, он будет выглядеть примерно так:
В этом случае возможность манипуляций с типами устраняется с помощью стандартного приема – путем присвоения интерфейсным типам конкретного, известного еще до исполнения программы типа.
Haskell – середина шкалы
Если рассматривать среднюю часть шкалы между «техническими» и «доменными» уязвимостями, Haskell демонстрирует возможности, которые, на мой взгляд, определяют преимущества от его выбора.
Прежде всего, в Haskell имеется возможность моделировать данные более точно по сравнению с такими языками, как как C, Javascript или даже Java. В основном это обусловлено удобством его синтаксиса и наличием типов-сумм. Точное моделирование данных имеет значение для безопасности, поскольку код домена в основном представляет собой модель некоторого реального явления. Чем меньше ее точность, тем больше возможностей имеют злоумышленники.
Наличие инструментов точного моделирования дает программистам возможность обходить ловушки в домене. Например, рассмотрим простую возможность легко выразить одной строчкой идею о том, что, скажем, номер страхового свидетельства неизвестен, либо скрыт, либо содержит указанное пользователем значение:
Аналогичные приемы могут помочь программистам повысить безопасность повсеместно. Продолжая предыдущий пример безопасного хранения пользовательских файлов на сервере – если ваша функция сохранения пользовательских загрузок начинается с
вероятность того, что вы создадите ситуацию, при которой пользователи могут сделать запись поверх ваших системных файлов, значительно снижается. Этот код скомпилируется, если только отсутствует практическая возможность, при которой путь к файлу содержит атаку с обходом каталога. Точно так же если после того, как пользователь потерпел неудачу при входе в систему, пользовательские данные просто недоступны для программы, или если вы просто не можете внедрить данные непроверенного пользователя в страницы HTML, вероятность ошибки снижается.
Я не утверждаю, что нельзя использовать другие языки для написания безопасного кода, и даже не говорю, что Haskell автоматически делает ваш код более безопасным. Я только считаю, что в Haskell имеются очень удобные инструменты, которые можно использовать для усиления защиты.
Haskell и ошибки домена
Чисто доменные ошибки выше описывались как ошибки, не зависящие от используемых инструментов. Это не совсем верно. Инструменты не выбираются случайным образом. Сообщества, объединяющие единомышленников, зачастую образуются вокруг различных инструментов. При этом такие сообщества могут иметь несхожие взгляды на безопасность.
В связи с этим в пользу Haskell говорит тот факт, что Haskell невозможно хорошо изучить случайным образом. На сегодняшний день Haskell остается достаточно редкой технологией, которая преподается даже не во всех университетах и входит не во все учебные планы. То есть если человек хорошо владеет Haskell, имеются основания предполагать, что он также имеет навык работы с формальными системами или интерес к компьютерным наукам. И хотя это не гарантирует, что программисты, работающие с Haskell, кое-что знают о безопасности, можно предположить, что при необходимости они смогут быстро разобраться в этом вопросе.
Однако это все догадки. Сообщество Haskell до сих пор достаточно мало, чтобы не быть объектом атак, а специалисты по Haskell в общем случае еще не так сильно озабочены проблемами безопасности, как разработчики на Javascript или Python.
Заключение
Конечно, Haskell имеет свои недостатки, и я не утверждаю, что другие языки не могут похвастаться такими же достоинствами. А в некоторых случаях, например, в случае атак по времени и прочих атак по сторонним каналам, другие инструменты могут обеспечить даже лучшую защиту. Кроме того, некоторые языковые сообщества в большей степени ориентированы на безопасность, чем Haskell. Но лично я считаю, что по сравнению с существующими универсальными языками программирования, доступными для выбора, Haskell предлагает очень хороший набор функций для разработки безопасного программного обеспечения.
Haskell
Helium, Gofer, O’Haskell, Haskell++, Mondrian, Disciple
Содержание
История
Хаскель принадлежит к семейству языков ML. Непосредственно на него оказал большое влияние язык Miranda, разработанный в 1985 г. Дэвидом Тёрнером. Миранда была первым чистым функциональным языком, имевшим коммерческую поддержку, и была относительно популярна в 1980-х годах, но оставалась несвободным программным обеспечением. Это затрудняло развитие и исследования возможностей ленивого функционального программирования, поэтому буквально за пару лет появилось более десятка схожих языков. Чтобы объединить усилия разных разработчиков, в 1987 г. на конференции по функциональным языкам программирования и компьютерной архитектуре в Орегоне (FPCA’87) было решено создать комитет для разработки открытого стандарта.
В 1990 г. была предложена первая версия языка, Haskell 1.0. В дальнейшем работа комитета продолжилась, и в 1999 г. был опубликован «The Haskell 98 Report [4] », который стал стабильным стандартом языка на много лет. Язык, однако, продолжал бурно развиваться, компилятор GHC был фактическим стандартом в отношении новых возможностей.
Сейчас разработка новых версий языка идёт открыто, этот процесс получил название Haskell’ [5] (Haskell Prime [ˈhæskəl praɪm], «Хаскель-штрих»). Все желающие могут выдвигать свои предложения к обсуждению, предложения обсуждаются в течение года, комитет отбирает и объявляет предложения, которые готов принять, формируется новый комитет и к концу года готовится новая версия языка. Таким образом, новые версии языка теперь могут появляться каждый год. Планируется объявлять некоторые ревизии «значительными» и поддерживать такие ревизии на протяжении длительного времени.
Характеристики языка
В качестве основных характеристик языка Haskell можно выделить следующие:
С момента принятия последнего стандарта языка (Haskell98) прошло много времени, и с тех пор ведущие реализации языка (ghc и hugs) были расширены множеством дополнительных возможностей:
Реализации языка
Компиляторы и интерпретаторы
Наиболее популярен на практике оптимизирующий компилятор GHC, который создаёт быстрый код и позволяет использовать многие расширения языка. GHC может оптимизировать как скорость, так и компактность программ, способен создавать многозадачный и параллелизованный код. В комплекте с компилятором GHC поставляется также интерактивная среда программирования GHCi со встроенным отладчиком. GHC работает в Windows, MacOS X и на нескольких юникс-подобных платформах (Linux, *BSD, Solaris). Именно GHC является стандартным компилятором в Haskell Platform, и именно на нём в первую очередь тестируются все новые библиотеки.
Другая популярная реализация языка — интерпретатор HUGS. Он написан на Си, имеет малый размер дистрибутива и работает практически на всех платформах. HUGS предоставляет интерактивную среду программирования, но может также запускать программы на Хаскеле в стиле скриптовых языков. Пользователи Windows могут использовать графическую интерактивную среду WinHugs. Поскольку HUGS интерпретатор, то программы, запущенные в нём, выполняются медленнее, чем код, созданный большинством компиляторов Хаскеля. HUGS часто рекомендуют в качестве среды для изучения языка. HUGS полностью поддерживает стандарт языка Haskell 98, а также некоторые наиболее популярные расширения языка.
Другие известные реализации:
Haskell Platform
В 2009 году сформировалась концепция Haskell Platform [9] — стандартного дистрибутива языка, включающего кроме компилятора (GHC), также дополнительный инструментарий (систему сборки и развёртывания пакетов Cabal) и набор популярных библиотек.
Сейчас Haskell Platform — это рекомендованный базовый дистрибутив для разработчиков. Готовые сборки Haskell Platform доступны для Windows, MacOS X и ряда дистрибутивов Linux.
Альтернативные целевые платформы
Большинство компиляторов Хаскеля создают непосредственно машинный код для используемой платформы, но есть несколько проектов, позволяющих компилировать Хаскель в код для виртуальных машин или генерировать код на других языках программирования. Степень зрелости и уровень поддержки подобных проектов сильно разнится.
Несколько интересных целевых платформ доступны при использовании компилятора YHC, в частности существуют интерпретатор байт-кода YHC на Питоне и конвертер байт-кода YHC в Erlang Core, но эти разработки пока ещё экспериментальны. Также существуют реализации подмножеств языка на разных целевых платформах.
Расширения языка
Расширения реализаций языка (относится к GHC):
Примеры
Вычисление факториала
Следующий пример показывает синтаксис языка Haskell при реализации функции для вычисления факториала:
Это определение описывает процесс вычисления факториала в виде рекурсивной функции. Это определение похоже на то, которое можно найти в учебниках по информатике. Большая часть исходного кода на языке Haskell походит на математическую нотацию в аспектах синтаксиса и использования, например, вышеприведённый пример можно переписать в виде
что соответствует математическому определению факториала.
Вторая строка основана на механизме сопоставления с образцами, который является важной особенностью языка Haskell. Этот механизм заставляет интерпретатор языка пробегаться сверху вниз по строкам определения и находить первый образец (то есть набор формальных параметров, который подходит под значения фактически переданных параметров в функцию) и выполнять определение, записанное с этим образцом. В данном случае вторая строка определения будет выбрана тогда, когда фактический параметр при вызове функции fac будет равен нулю.
Калькулятор
Простейший калькулятор для вычисления выражений в обратной польской записи может быть определён на языке Haskell при помощи одной функции:
Числа Фибоначчи
Другой пример показывает способ вычисления бесконечного списка чисел Фибоначчи за линейное время:
Это определение является примером применения механизма ленивых вычислений, который является важнейшей частью языка Haskell. Для понимания того, как это определение работает, можно рассмотреть вычисление первых семи чисел Фибоначчи с его помощью:
То же самое может быть записано также при использовании определителей списков,
или расширения языка Haskell, реализованного в компиляторе GHC (параллелизация определителей списков, Parallel List Comprehensions):
Простые числа
В этих примерах показано, как можно использовать списочные выражения (генераторы списков). Реализация нахождения всех простых чисел обычным путём (проверка каждого числа на простоту):
а также с помощью решета Эратосфена, в варианте ограниченного списка,
или, вообще говоря, бесконечного списка простых чисел:
Описание игральных карт
Численное интегрирование
Численное интегрирование методом трапеций:
Проверка палиндромов
Как видно, Хаскель прекрасно работает с Юникодом.
Приложения, написанные на языке Haskell
Мозаичный оконный менеджер Xmonad для X Window System целиком написан на Хаскеле. Darcs — распределённая система управления версиями с рядом уникальных возможностей — написана на Хаскеле. Первая реализация компилятора и интерпретатора языка Perl 6, Pugs, была написана на Хаскеле за несколько месяцев. Компилятор GHC часто выступает экспериментальной площадкой для проверки новых возможностей функционального программирования и оптимизации.
Коммерческие приложения
Приложения с открытым исходным кодом
Также на Хаскеле написано много приложений c открытым исходным кодом. Большинство из них доступны в архиве Hackage, ниже приведены некоторые из них.
Через тернии к Haskell. 1/2
Первая часть короткого и жесткого введения в Haskell. Вторую часть можно найти здесь
Основа мейнстримовых языков очень похожа
Haskell сильно от них отличается. Этот язык использует кучу концепций, о которых я раньше не слышал. Многие из этих концепций помогут вам стать лучшим программистом.
Но изучение Haskell-а может быть сложным. По крайней мере оно было сложным для меня. И в этой статье я постараюсь показать, чего мне не хватало в процессе изучения.
Эту статью будет сложно осилить. Это было сделано специально. В изучении Haskell нет простых путей. Эта дорога трудна и заковыриста. Но я думаю, что это и хорошо. Потому что именно сложность Haskell-а делает его интересным.
Обычный метод изучения Haskell-а состоит в прочтении двух книг.
Сначала “Learn You a Haskell”, а потом “Real World Haskell”.
Я согласен с тем, что это правильный подход к обучению. Но для того, чтобы понять, что из себя представляет Haskell, вам придется очень внимательно прочитать эти книги.
С другой стороны, эта статья очень краткое и сжатое ввдение во все основные аспекты Haskell. Я также добавил некоторые нюансы, которых мне не хватало при изучении Haskell.
Статья состоит из пяти частей:
Какие-то примеры могут не работать, но большинство должно.
Чуть ниже вы должны увидеть ссылку на файл
Введение
Установка
Без паники
Многие книги и статьи начинают знакомство с Haskell-ом с каких-то эзотерических формул (быстрая сортировка, Фибоначчи и т.п.). Я поступлю по-другому. Я не буду сразу показывать суперсилу Haskell-а. Я сконцентрируюсь на тех вещах, в которых Haskell похож на остальные языки программирования. Итак, давайте начнем с привычного “Hello World”.
Чтобы запустить его, сохраните этот код как hello.hs и выполните:
Вы можете скачать исходник по ссылке, которая находится под «Введением»
Сохраняем файл 00_hello_world.lhs и выполняем:
А теперь напишем программу, которая запрашивает ваше имя и говорит “Привет, имярек!”:
Для начала, сравним наш пример с другими императивными языками:
Структура программ очень похожа, но есть небольшие синтаксические отличия. Основная часть туториала будет посвящена именно этим отличиям.
Но не забывайте о том, что Haskell может выглять как обычный императивный язык.
Самый базовый Haskell
Перед тем, как мы продолжим, хотелось бы предупредить вас о некоторых характерных особенностях Haskell.
Haskell — это функциональный язык.
Если вы начинали с императивных языков, вам придется выучить много новых вещей.
К счастью, многие из эти концепций помогут вам лучше программировать даже на императивных языках.
Продвинутая статическая типизация
Вообще говоря, ваши функции ничего не будут изменять во внешнем мире.
С одной строны, это значит что функции не могут изменять значение переменной, не могут получать данные от пользователя, не могут писать на экране, не могут запускать ракеты.
С другой стороны, мы получаем легкое распараллеливание наших программ.
Haskell проводит четкую границу между чистыми функциями и функциями, производящими побочные эффекты.
При таком подходе становится намного легче анализировать свойства вашей программы.
В функционально чистых частях программы многие ошибки будут исключены еще на этапе компиляции.
Более того, функционально чистые функции следуют основному закону Haskell-а:
Вызов функции с одними и теми же параметрами всегда даст один и тот же результат.
Ленивость по умолчанию очень необычная вещь в языках программирования.
По умолчания, Haskell вычисляет значение только тогда, когда оно реально необходимо.
Как следствие, мы получаем возможность очень просто работать с бесконечными структурами данных.
Определение функций
Вы можете определять функции следующим образом:
Не забывайте, что в Haskell интенсивно используются функции и типы.
И поэтому их очень просто определять.
Синтаксис языка был продуман с тем, чтобы сделать определения максимально простыми.
Пример создания нового типа
Обычно при написании программ, вы указываете тип функции.
Но это не обязательно.
Компилятор достаточно умен, чтобы сделать это за вас.
Давайте немного поиграемся.
Вы получите ошибку:
Проблема заключается в том, что 4.2 это не Int.
Но какой тип мы должны указать?
Чтобы понять, какой тип для нас вывел Haskell, просто запустите ghci:
Хмм… Что это за странный тип?
Классы типов — очень мощный элемент языка.
С их помощью мы можем творить удивительные вещи.
Но об этом мы поговорим чуть позже.
Мде, странновато.
На самом деле в Haskell-е ни одна функция не принимает на вход два аргумента.
Вместо этого у всех функций — только один аргумент.
Но обратите внимание, что функцию от двух аргументов можно представить как функцию, принимающую первый аргумент и возвращающую функцию, принимающую второй аргумент в качестве параметра.
.
Для создания функция также можно использовать другую запись.
Лямбда-выражения позволяют создавать функции не присваивая им имен.
Их также назызвают анонимными функциями.
Мы можем записать:
Если функциональное программирование вам в новинку, к этому моменту ваши мозги начинают закипать.
Самое время написать реальное приложение.
Но прежде, чем мы начнем, надо проверить, что система типов работает как следует:
Этот код работает потому, что 3 может представлять как Fractional (дробное) число типа Float, так и целое типа Integer.
Поскольку 2.4 имеет тип Fractional, 3 тоже представляется в виде Fractional числа.
Если мы пропишем в функции другие типы, она перестанет работать:
Компилятор сигнализирует об ошибке.
Два параметра должны иметь одинаковый тип.
Если вы считаете, что это плохая идея, и комплятор должен преобразовать типы за вас, вам действительно стоит посмотреть отличное и смешное видео:
WAT
Необходимый минимум Haskell
Я советую вам бегло просмотреть этот раздел.
Думайте о нем, как о справочном материале.
Haskell — очень многоплановый язык.
Поэтому какие-то вещи в этом разделе будет пропущены.
По необходимости возвращайтесь сюда, если какие-то вещи покажутся вам странными или непонятными.
Выражения
Арифметические
Логические
Возведение в степень
Integer не имеет ограничений по величине, за исключением оперативной памяти машины:
О да!
А еще можно использовать рациональные числа!
Но для этого необходимо использовать модуль Data.Ratio :
Списки
Строки
Кортежи
Разбираемся со скобками
Полезные вещи для записи функций
Объявлять тип функции перед ее определеним необязательно.
Haskell сам выведет наиболее общий тип.
Но указание типа функции — правило хорошего тона.
Обратите внимание, что ^ используется в инфиксной нотации.
Для каждого инфиксного оператора существует возможность префиксной записи.
Просто заключите нужный оператор в скобки.
Мы можем убрать x из левой и правой части выражения!
Это называется η-редукция.
Обратите внимание, что мы можем использовать символ ‘ в имени функции.
Например:
Еще один эквивалент функции:
Внимание: выравнивание важно в программах на Haskell-е.
Как и в Python-е, неправильное выравнивание может сломать код!
Сложная часть
Приступим к изучению сложной части.
Функциональный стиль
В этом разделе я покажу вам небольшой пример удивительных возможностей Haskell по рефакторингу кода.
Мы выберем проблему и решим ее стандартным императивным способом. Потом начнем понемногу улучшать этот код. Конечный результат будет гораздо проще для понимании и более элегантным.
Вот условие задачи, которую мы будем решать:
Имея список целых чисел, необходимо посчитать сумму четных чисел в списке.
Для того, чтобы показать разницу между функциональным и императивным подходом, сначала напишем императивное решение (на Javascript):
Но в Haskell-е отсутствуют переменные и циклы.
Без использования циклов тот же результат можно получить, используя рекурсию.
Замечание:
Рекурсию в императивных языках имеет репутацию медленного инструмента. Но для большинства функциональных языков это не так. В большинстве случаев, Haskell очень качественно оптимизирует работу с рекурсивными функциями.
Внимательно посмотрите на этот код. Потому что мы будем переводить его на Haskell.
Но прежде я покажу вам три простые, но очень полезные функции, которые мы будем использовать:
even проверка на четность.
head возвращает первый элемент списка:
tail возвращает все элементы списка, окромя первого:
Итак, первое решение задачи на Haskell-е.
Функция evenSum возвращает сумму всех четных чисел в списке:
Чтобы протестировать функцию, просто запустите ghci :
Ниже представлен пример работы функции (Я жульничаю. В курсе. К вопросу о нестрогих вычислениях мы вернемся позже):
С точки зерния императивного языка все выглядит нормально. Но тут кроется большой простор для улучшений. Для начала, мы можем сделать тип более общим.
А теперь используем сопоставление с образцом.
Что такое сопоставление с образцом (pattern matching)? Это просто ипользование значений вместо именованных параметров. (Для самых смелых подробное описание сопоставления с образцом можно изучитьздесь)
Вместо такого кода: foo l = if l == [] then else
Можно просто написать:
Но сопоставление с образцом может гораздо большее. Оно может анализировать внутренние данные сложной структуры. Мы можем заменить
Очень полезная возможность.
Она делает наш код более кратким и читаемым.
Вы можете упростить запись функций в Haskell-е применяя η-редукцию.
Например, вместо такой записи:
Мы можем использовать этот подход для того, чтобы избавиться от l :
Функции Высшего Порядка
Чтобы сделать нашу функцию еще более красивой, мы можем применить ФВП (функции высшего порядка).
Вы спрашиваете, что это за звери?
Функции высшего порядка — это функции, принимающие функции в качестве параметра.
Будем продвигаться небольшими шагами.
Код выше можно заменить на:
Если вы действительно хотите разобраться в происходящей магии,
реализация foldl приведена ниже.
Если для вас не очевидна разность между ленивыми и строгими функциями, не беспокойтесь, просто читайте код, считая что обе функции одинаковы.
Теперь наша версия evenSum выглядит так:
И конечно, мы можем провести следующую замену
foldl’ не самая интуитивно понятная функция. Но с ней обязательно стоит разобраться.
Для этого проведем пошаговый анализ:
Мы можем ее использовать, чтобы еще дальше η-редуцировать нашу функцию:
Можно сделать еще кое-что, чтобы код стал чище:
Обсудим получившийся результат.
Что мы получили, применив функции высшего порядка?
В глаза бросается краткость кода. Но основной плюс в том, насколько проще стало понимать нашу программу. Допустим, мы хотим немного изменить нашу функцию. Теперь нам хочется, чтобы она возвращала сумму квадратов всех четных чисел в списке.
Добавить это изменение в версию 10 очень просто:
Нам надо просто добавить еще одну “преобразовывающую функцию” (Обратите внимание, что squareEvenSum» эффективнее двух других реализаций. Порядок (.) действительно имеет значение.).
Функция map просто применяет функцию-параметр ко всем элементам списка.
Если вы думаете, что сделать код более абстрактным сложно, то вы сильно ошибаетесь.Например, эту функцию можно использовать не только для списков, но и для любых рекурсивных типов. Если вам интересно как — почитайте эту забавную статью: Functional Programming with Bananas, Lenses, Envelopes and Barbed Wire by Meijer, Fokkinga and Paterson.
На нашем примере можно увидеть, насколько мощным может быть функциональное программирование. К сожалению в некоторых случаях чисто функциональные языки не всегда самый лучший выбор. По крайней мере, действительно универсальный функциональный язык еще не создан.
Одна из отличительных особенностей Haskell лежит в создании DSL-ей (Доменно Ориентированных Языков).
На самом деел, Haskell является отличным инструментом, даже если вы хотите писать программы в императивном стиле. Когда я изучал Haskell, эта мысль стала для меня откровением.
Но перед тем, как мы перейдем к разговорам о суперсиле Haskell-а, нам надо обсудить еще один существенный аспект — Типы.
Haskell — язык со строгой статической типизацией.
Почему это так важно? Потому что это сильно уменьшает количество ошибок.
В Haskell, большинство ошибок можно обнаружить еще на этапе компиляции. И происходит это потому, что во время компиляции активно применяется вывод типов.
Если вы используете не тот параметр не в том месте, компилятор это легко заметит.
Вывод типов
Статическая типизация необходима для создания быстрых программ
Но большинство статически типизированных языков слабы в обобщении концепций.
Одна из благодатных возможностей Haskell-а вывод типов.
Простой пример.
Функция square на языке Haskell:
x :+ y это запись комплексного числа (x + iy).
Теперь сравните количество кода по сравнению с программой на C:
Для каждого типа надо писать свою функцию. Обойти это можно только используя техники метапрограммирования. Например используя препроцессор. C++ предлагает более красивый вариант решения, используя шаблоны:
Вариант на C++ выглядит гораздо лучше C.
Но для функций комплексной переменной будет сложно сохранить такой же синтаксис. Пример можно найти в
этой статье
.
В C++ вы должны описать функцию, которая может работать с разными типами.
В Haskell ситуация противоположная.
Функция будет максимально обобщенной по умолчанию.
Вывод типов Haskell дает ощущение свободы, которое представляют динамически типизированные языки.
Но в отличие от динамически типизированных языков, большинство ошибок, отлавливаются еще до момента выполнения программы.
Люди говорят про Haskell так:
“Если оно компилируется, то оно скорее всего работает правильно”
Создание новых типов
Вы можете создавать свои собственные типы. Например, можно объявлять синонимы типов.
Но это не сильно защитит вас от ошибок.
Попробуйте поменять местами два параметра функции showInfos и запустить программу:
Если в showInfos поменять параметры местами, компилятор начнет ругаться. Возможность ошибиться исчезла. Но ценой увеличения количества кода.
Обратите внимание, что конструкторы типа — это функции :
Использование data обычно выглядит так:
Для
DataTypeName и DataTypeConstructor обычно используют одно и то же имя.
Например:
Также можно использовать синтаксис записей:
И куча функций доступа к данным будет создана автоматически. Более того, при записи новых значений вы можете использовать произвольный порядок полей.
Рекурсивные типы
Вы уже встречались с представителем рекурсивных типов — со списками. Можно создать списки с нуля, используя более многословный синтаксис:
Для более простой записи можно использовать инфиксный конструктор типа.
Если вы хотите печатать ( Show ), считывать ( Read ), проверять на равенство ( Eq ) и сравнивать ( Ord ) значения вашего нового типа, вы можете сказать Haskell-у, чтобы он автоматически вывел необходимые для этого функции.
Деревья
Вот еще один стандартный пример: двоичные деревья.
Давайте напишем функцию, которая преобразует список в упорядоченное двоичное дерево.
Оцените элегантность этой функции.
Перевод на русский язык:
Должно получиться что-то вроде:
Это информативное, но не очень красивое представление нашего дерева.
Чтобы немного развлечься, давайте напишем код для более красивого отображения наших деревьев. Я получил большое удовольствие от написания функции, которая может красиво отображать любые деревья. Если эта часть покажется вам сложной — можете спокойно ее пропустить.
Вот моя версия отображения бинарного дерева. Она не так страшна, как выглядит.Я адаптировал ее для отображения гораздо более странных объектов.
Метод treeFromList не изменяется.
А теперь мы можем поиграться:
А поскольку мы можем проверять деревья на равенство и задавать их порядок, мы можем создать дерево деревьев!
Что эквивалентно следующему:
Посмотрите на красоту и мощь этого типа. Мы можем создавать деревья состоящие не только из чисел, строк и символов, но так же из других деревьев. Мы можем даже создать дерево, элементами которого будут деревья деревьев!
Бесконечные структуры
Haskell часто называют ленивым языком.
Но правильно говорить, что Haskell это не строгий язык. Ленивость это просто популярная реализация нестрогих языков.
Что такое нестрогий язык? Из Haskell-wiki:
Редукция (математический термин для вычисления выражения) идет снаружи внутрь.
Например используя Haskell можно сделать так:
И программа завершается.
Обратите внимание на синтаксис для задания бесконечных списков в Haskell
Допустим, мы не против иметь упорядоченное двоичное дерево. Вот пример бесконечного двоичного дерева:
Полноценное двоичное дерево с нулями в каждом узле. Теперь я докажу вам, что мы можем работать с этим деревом, используя следующую функцию:
Давайте посмотрим, что выведет эта программа:
Этот код компилируется, запускается, останавливается и выводит следующий результат:
Чтобы немного раскачать нейроны, сделаем дерево более странным: