34
À. À. Êóáåíñêèé Ìîñêâà Þðàéò 2017 ÔÓÍÊÖÈÎÍÀËÜÍÎÅ ÏÐÎÃÐÀÌÌÈÐÎÂÀÍÈÅ УЧЕБНИК И ПРАКТИКУМ ДЛЯ АКАДЕМИЧЕСКОГО БАКАЛАВРИАТА Ðåêîìåíäîâàíî Ó÷åáíî-ìåòîäè÷åñêèì îòäåëîì âûñøåãî îáðàçîâàíèÿ â êà÷åñòâå ó÷åáíèêà è ïðàêòèêóìà äëÿ ñòóäåíòîâ âûñøèõ ó÷åáíûõ çàâåäåíèé, îáó÷àþùèõñÿ ïî èíæåíåðíî-òåõíè÷åñêèì íàïðàâëåíèÿì Êíèãà äîñòóïíà â ýëåêòðîííîé áèáëèîòå÷íîé ñèñòåìå biblio-online.ru

ÔÓÍÊÖÈÎÍÀËÜÍÎÅ ÏÐÎÃÐÀÌÌÈÐÎÂÀÍÈÅ · рования конструкции для параллельного выполнения фрагментов

  • Upload
    others

  • View
    10

  • Download
    0

Embed Size (px)

Citation preview

Page 1: ÔÓÍÊÖÈÎÍÀËÜÍÎÅ ÏÐÎÃÐÀÌÌÈÐÎÂÀÍÈÅ · рования конструкции для параллельного выполнения фрагментов

À. À. Êóáåíñêèé

Ìîñêâà Þðàéò 2017

ÔÓÍÊÖÈÎÍÀËÜÍÎÅ ÏÐÎÃÐÀÌÌÈÐÎÂÀÍÈÅ

УЧЕБНИК И ПРАКТИКУМ ДЛЯ АКАДЕМИЧЕСКОГО БАКАЛАВРИАТА

Ðåêîìåíäîâàíî Ó÷åáíî-ìåòîäè÷åñêèì îòäåëîì âûñøåãî îáðàçîâàíèÿ â êà÷åñòâå ó÷åáíèêà è ïðàêòèêóìà äëÿ ñòóäåíòîâ âûñøèõ ó÷åáíûõ

çàâåäåíèé, îáó÷àþùèõñÿ ïî èíæåíåðíî-òåõíè÷åñêèì íàïðàâëåíèÿì

Êíèãà äîñòóïíà â ýëåêòðîííîé áèáëèîòå÷íîé ñèñòåìåbiblio-online.ru

Page 2: ÔÓÍÊÖÈÎÍÀËÜÍÎÅ ÏÐÎÃÐÀÌÌÈÐÎÂÀÍÈÅ · рования конструкции для параллельного выполнения фрагментов

УДК 004.438(075.8)ББК 32.973-018я73 К98

Автор:Кубенский Александр Александрович — доцент, кандидат физико-математи-

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

К98 Кубенский, А. А.

Функциональное программирование : учебник и практикум для академического бакалавриата / А. А. Кубенский. — М. : Издательство Юрайт, 2017. — 348 с. — Серия : Бакалавр. Академический курс.

ISBN 978-5-9916-9242-7

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

Содержание учебника соответствует актуальным требованиям Федерального государственного образовательного стандарта высшего образования.

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

УДК 004.438(075.8) ББК 32.973-018я73

ISBN 978-5-9916-9242-7© Кубенский А. А., 2016© ООО «Издательство Юрайт», 2017

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

Page 3: ÔÓÍÊÖÈÎÍÀËÜÍÎÅ ÏÐÎÃÐÀÌÌÈÐÎÂÀÍÈÅ · рования конструкции для параллельного выполнения фрагментов

3

Оãëавëение

Предисловие ..................................................................................... 6Введение........................................................................................... 8Глава 1. Зачем нужно функцио нальное программирование .................11

1.1. Особенности функцио нального стиля ......................................................................11Примеры решения задач .........................................................................................................16Задачи для самостоятельного решения ............................................................................21

Глава 2. Элемен ты языка Haskell .......................................................232.1. Система программирования Haskell Platform ........................................................232.2. Элемен тарные типы данных .........................................................................................262.3. Определение функций с помощью уравнений .......................................................322.4. Концевая рекурсия и накапливающие аргумен ты ................................................382.5. Техника работы со списками ........................................................................................43Примеры решения задач .........................................................................................................52Задачи для самостоятельного решения ............................................................................59

Глава 3. Функции высших порядков ..................................................613.1. Отображение и свертка. Лямбда-выражения .........................................................613.2. Обработка списков с помощью функций высших порядков .............................71Примеры решения задач .........................................................................................................77Задачи для самостоятельного решения ............................................................................83

Глава 4. Определение новых типов данных .........................................844.1. Определение типов данных ...........................................................................................844.2. Использование функций высших порядков при обработке сложных

структур ...............................................................................................................................94Примеры решения задач ...................................................................................................... 103Задачи для самостоятельного решения ......................................................................... 108

Глава 5. Типы и классы ................................................................... 1105.1. Определение классов .................................................................................................... 1105.2. Вычисления с неопределенным результатом ...................................................... 116Примеры решения задач ...................................................................................................... 122Задачи для самостоятельного решения ......................................................................... 126

Глава 6. Частичная параметризация функций ................................... 1286.1. Карринг ............................................................................................................................. 1286.2. Функцио нальное представление данных .............................................................. 1346.3. Позиционирование в списках .................................................................................... 142Примеры решения задач ...................................................................................................... 146Задачи для самостоятельного решения ......................................................................... 165

Page 4: ÔÓÍÊÖÈÎÍÀËÜÍÎÅ ÏÐÎÃÐÀÌÌÈÐÎÂÀÍÈÅ · рования конструкции для параллельного выполнения фрагментов

4

Глава 7. Ленивые вычисления .......................................................... 1677.1. Энергичная и ленивая схемы вычислений ........................................................... 1677.2. Бесконечные структуры данных .............................................................................. 174Примеры решения задач ...................................................................................................... 178Задачи для самостоятельного решения ......................................................................... 181

Глава 8. Функторы и монады ........................................................... 1828.1. Функторы ......................................................................................................................... 1828.2. Монады и последовательные вычисления ............................................................ 1908.3. Ввод-вывод. Компиляция программ на Haskell .................................................. 198Примеры решения задач ...................................................................................................... 204Задачи для самостоятельного решения ......................................................................... 211

Глава 9. Лямбда-исчисление ............................................................ 2139.1. Представление выражений в лямбда-исчислении ............................................. 2139.2. Нормальная форма ........................................................................................................ 2169.3. Слабая заголовочная нормальная форма .............................................................. 2239.4. Рекурсия в лямбда-исчислении ................................................................................ 2279.5. Чистое лямбда-исчисление ........................................................................................ 231Примеры решения задач ...................................................................................................... 236Задачи для самостоятельного решения ......................................................................... 239

Глава 10. Представление функцио нальных программ ........................ 24010.1. Расширенное лямбда-исчисление ..........................................................................24010.2. Представление программ в расширенном лямбда-исчислении ...................242Примеры решения задач ...................................................................................................... 250Задачи для самостоятельного решения ......................................................................... 256

Глава 11. Интерпретация функцио нальной программы ...................... 25711.1. Eval/apply-интерпретатор Маккарти ....................................................................257Примеры решения задач ...................................................................................................... 269Задачи для самостоятельного решения ......................................................................... 271

Глава 12. SECD-машина и исполнение функцио нальных программ .... 27312.1. Архитектура SECD-машины ....................................................................................27312.2. Ленивая версия SECD-машины ..............................................................................28312.3. Компиляция функцио нальных программ в SECD-машину .........................286Примеры решения задач ...................................................................................................... 298Задачи для самостоятельного решения ......................................................................... 299

Глава 13. Функцио наль ные эквиваленты императивных программ ..... 30013.1. Абстрактный императивный язык программирования ..................................30013.2. Императивная программа как функция ...............................................................302Пример решения задач ......................................................................................................... 306Задачи для самостоятельного решения ......................................................................... 307

Глава 14. Графическое представление функцио нальных программ..... 30814.1. Графическое представление конструкций расширенного лямбда-

исчисления .....................................................................................................................30814.2. Преобразование графов при исполнении программ ........................................31214.3. Функции-проекторы и фиктивные узлы .............................................................316

Page 5: ÔÓÍÊÖÈÎÍÀËÜÍÎÅ ÏÐÎÃÐÀÌÌÈÐÎÂÀÍÈÅ · рования конструкции для параллельного выполнения фрагментов

Примеры решения задач ...................................................................................................... 319Задачи для самостоятельного решения ......................................................................... 321

Глава 15. Комбинаторная редукция ................................................. 32215.1. Основные комбинаторы .............................................................................................32215.2. Абстрагирование от переменных ............................................................................32415.3. Оптимизации Карри ....................................................................................................32815.4. Сохранение аппликативных подвыражений при преобразованиях ...........331Примеры решения задач ...................................................................................................... 332Задачи для самостоятельного решения ......................................................................... 334

Глава 16. Комбинаторная редукция на графах .................................. 33516.1. Правила преобразования графов для основных комбинаторов ...................33516.2. Представление рекурсивных функций при редукции на графах ................338Примеры решения задач ...................................................................................................... 341Задачи для самостоятельного решения ......................................................................... 345

Список рекомендуемой литературы ................................................. 347Новые издания по дисциплине «Функциональное программирование» и смежным дисциплинам .................................. 348

Page 6: ÔÓÍÊÖÈÎÍÀËÜÍÎÅ ÏÐÎÃÐÀÌÌÈÐÎÂÀÍÈÅ · рования конструкции для параллельного выполнения фрагментов

6

Преäисëовие

Эта книга представляет собой учебник по курсу «Функцио нальное про-граммирование» для студентов 3—4-го курсов технических университетов. Она представляет собой сконцентрированный многолетний опыт препо-давания курса функцио нального программирования для студентов Санкт-Петербургского государственного университета и Санкт-Петербургского нацио нального исследовательского университета информационных техно-логий, механики и оптики.

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

Предполагается, что студенты, изучающие функцио нальное программи-рование, уже знают основы программирования на одном из распространен-ных языков программирования (Паскаль, C/C++, Java, C#), умеют писать рекурсивные функции, знакомы с понятиями объектно-ориентированного стиля программирования. Требуется также знание основ математической логики, первичное ознакомление с понятиями логического вывода и аксио-матического подхода. После завершения курса студенты будут готовы при-менять элемен ты функцио нального программирования на практике, в том числе и на языках, не предназначенных исключительно для программиро-вания в функцио нальном стиле.

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

от традиционного императивного стиля;• основы языка функцио нального программирования Haskell;• методы интерпретации и компиляции программ, написанных

на функцио нальных языках;• основные понятия лямбда-исчисления;• основы комбинаторного стиля в функцио нальном программирова-

нии, понятия комбинаторной логики;уметь• составлять несложные программы в функцио нальном стиле на языке

Haskell;• пользоваться приемами и средствами функцио нального программи-

рования в современных языках программирования;• переводить лямбда-выражения в комбинаторную форму;

Page 7: ÔÓÍÊÖÈÎÍÀËÜÍÎÅ ÏÐÎÃÐÀÌÌÈÐÎÂÀÍÈÅ · рования конструкции для параллельного выполнения фрагментов

владеть• навыками применения функцио нального стиля программирования

для составления алгоритмов;• методами программирования на языке Haskell;• способами преобразования функцио нальных программ.

Page 8: ÔÓÍÊÖÈÎÍÀËÜÍÎÅ ÏÐÎÃÐÀÌÌÈÐÎÂÀÍÈÅ · рования конструкции для параллельного выполнения фрагментов

8

Ввеäение

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

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

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

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

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

Page 9: ÔÓÍÊÖÈÎÍÀËÜÍÎÅ ÏÐÎÃÐÀÌÌÈÐÎÂÀÍÈÅ · рования конструкции для параллельного выполнения фрагментов

9

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

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

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

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

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

Page 10: ÔÓÍÊÖÈÎÍÀËÜÍÎÅ ÏÐÎÃÐÀÌÌÈÐÎÂÀÍÈÅ · рования конструкции для параллельного выполнения фрагментов

10

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

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

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

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

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

Page 11: ÔÓÍÊÖÈÎÍÀËÜÍÎÅ ÏÐÎÃÐÀÌÌÈÐÎÂÀÍÈÅ · рования конструкции для параллельного выполнения фрагментов

11

Гëава 1. ЗАЧЕМ НУЖНО ФУНКЦИО НАЛЬНОЕ

ПРОГРАММИРОВАНИЕ

В результате изучения материала главы 1 студент должен:знать• особенности функцио нального стиля программирования;• недостатки и преимущества функцио нального стиля по отношению к импера-

тивному стилю программирования;уметь• преобразовывать программы, написанные в императивном стиле, в программы,

написанные в функцио нальном стиле;• оптимизировать функцио наль ные программы для параллельных вычислений;владеть• навыками анализа кода на предмет соответствия его функцио нальному стилю

программирования.

1.1. Особенности функцио наëьноãо стиëя

Рассмотрим основные особенности функцио нального программирования.Часто процесс исполнения программы можно представить в виде схемы,

показанной на риc. 1.1.Входные данные

Выходные данныеПрограмма

Риc. 1.1. Схема простой функцио нальной программы

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

Можно привести много примеров простых и сложных программ, име-ющих смысл и работающих по этой схеме. Например, в программе анали-тических преобразований выражений входными и выходными данными будут выражения, представленные в разной форме. Другой пример —

Page 12: ÔÓÍÊÖÈÎÍÀËÜÍÎÅ ÏÐÎÃÐÀÌÌÈÐÎÂÀÍÈÅ · рования конструкции для параллельного выполнения фрагментов

12

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

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

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

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

• каждая функция в программе выдает один и тот же результат на одном и том же наборе входных данных (аргумен тов функции), т.е. результат работы функции является «повторяемым»;

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

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

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

Page 13: ÔÓÍÊÖÈÎÍÀËÜÍÎÅ ÏÐÎÃÐÀÌÌÈÐÎÂÀÍÈÅ · рования конструкции для параллельного выполнения фрагментов

13

это свойство функцио нального стиля программирования привлекает к нему все возрастающий интерес.

Можно ли писать программы в функцио нальном стиле на традицион-ном языке программирования? Конечно, да. Если программа представляет собой набор чистых детерминированных функций, то она будет «функцио-нальной» независимо от того, написана ли она на специальном языке функцио нального программирования Haskell или на традиционном Java. Рассмотрим, например, задачу вычисления суммы вещественных элемен-тов списка. Функция, решающая эту задачу, должна, получив в качестве исходных данных список из вещественных чисел, вычислить и выдать в качестве результата сумму элемен тов списка. На языке Java функция может выглядеть следующим образом (листинг 1.1).

Листинг 1.1. Функция вычисления суммы элемен тов числового спискаdouble sumList(List<Double> list) { double sum = 0; for (Double element : list) { sum += element; } return sum;}

Эта функция, конечно же, детерминированная и «чистая», она выдает всегда один и тот же результат на одинаковых входных данных и не вли-яет на поведение других функций. Однако все же с точки зрения функцио-нального стиля она имеет одну «неправильность». Дело в том, что в функ-ции определяется и используется локальная переменная sum, которая нужна для запоминания промежуточных значений суммы. В данном случае это никак не влияет на «чистоту» написанной функции, но то, что пере-менная изменяет значения по ходу работы, означает, что последователь-ность выполнения действий имеет существенное значение. «Распаралле-лить» работу такой функции очень трудно, если не невозможно. Во всяком случае, для этого требуется серьезный анализ смысла производимых дей-ствий. Мы видим, что присваивание переменным новых значений приво-дит к тому, что существенное значение начинает иметь последовательность выполнения действий.

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

Впрочем, в нашем случае исправить программу, приведя ее в соответ-ствие с принципами функцио нального стиля программирования, можно. Для этого определим нашу функцию следующим образом (листинг 1.2).

Page 14: ÔÓÍÊÖÈÎÍÀËÜÍÎÅ ÏÐÎÃÐÀÌÌÈÐÎÂÀÍÈÅ · рования конструкции для параллельного выполнения фрагментов

14

Листинг 1.2. Рекурсивная функция вычисления суммы элемен тов спискаdouble sumList(List<Double> list) { final int size = list.size(); final int mid = size / 2; return size == 0 ? 0 : size == 1 ? list.get(0) : sumList(list.subList(0, mid)) + sumList(list.subList(mid, size));}

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

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

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

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

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

Page 15: ÔÓÍÊÖÈÎÍÀËÜÍÎÅ ÏÐÎÃÐÀÌÌÈÐÎÂÀÍÈÅ · рования конструкции для параллельного выполнения фрагментов

15

нужно описать инструмен т, с помощью которого из двух произвольных функций f(x) и g(x) можно было бы получить новую функцию fg(x), результат работы которой определялся бы уравнением fg(x) = f(g(x)). Конечно, решение этой задачи было бы очень полезным в ситуации, когда практически единственным инструмен том программирования является определение и вызов различных функций. В последних версиях языка Java, начиная с версии Java 8, имеется довольно широкий набор средств функцио нального программирования, так что поставленную задачу можно решить довольно просто (функция образования суперпозиции двух других функций включена в стандартную библиотеку программ этой версии Java). В листинге 1.3 приведена искомая функция.

Листинг 1.3. Реализация суперпозиции на Javastatic <T, U, V> Function<T,V> compose(Function<U,V> f, Function<T,U> g) { return x -> f.apply(g.apply(x));}

Даже если вы не очень хорошо разбираетесь во всех деталях запи си, общий смысл написанного понять можно. Результатом работы этой функ-ции является новая функция с аргумен том x, результат работы которой — последовательное применение (apply) функций g и f к этому аргумен ту. Видно, впрочем, что на самом деле аргумен ты f и g — это не совсем функ-ции: запись f(g(x)) синтаксически неверна. Объекты типа Function — это сложно устроенные объекты, внутри которых и спрятана исполняемая функция. Несмотря на внешнюю простоту запи си, в приведенной реализа-ции имеется много скрытых деталей, которые необходимы для того, чтобы весьма сложно организованный объект имел вид обычной функции. Напри-мер, если подумать, то становится непонятно, почему аргумен ты функции comp — переданные ей функции f и g — не пропадают после выхода из этой функции, а остаются «жить» внутри возвращаемого результата.

Немного странным выглядит и использование функции compose в довольно простой ситуации. Естественная на первый взгляд запись compose(Math.sin, Math.cos)(3.14) оказывается синтаксически неверной. Правильной будет запись compose(Math::sin, Math::cos).apply(3.14). Очень хорошо видно, что используемые нами «функции» — это не совсем функции в традиционном понимании, и в язык Java при-шлось добавить много нового синтаксиса для того, чтобы выразить такие манипуляции с функциями, которые в функцио нальных языках выглядят очень просто и естественно, да и реализуются просто.

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

Page 16: ÔÓÍÊÖÈÎÍÀËÜÍÎÅ ÏÐÎÃÐÀÌÌÈÐÎÂÀÍÈÅ · рования конструкции для параллельного выполнения фрагментов

16

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

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

Приìеры решения заäа÷

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

Задача 1.1. Напишите программу для вычисления приближенного зна-чения числа e по формуле для разложения ex в ряд Тейлора. Предложите программы на языке Java, написанные в традиционном императивном и функцио нальном стилях.

Решение. Данная задача является примером задачи вычисления значе-ния по заданной функцио нальной зависимости, в данном случае, вычисле-ния значения ex по заданному аргумен ту x. Фа ктически, поскольку вычис-ления осуществляются приближенно, необходимо ввести дополнительный аргумен т, определяющий точность вычислений. Будем считать, что задано также вещественное значение eps, причем суммирование ряда Тейлора будет прекращено, когда очередной член ряда станет по абсолютной вели-чине меньше eps. Тогда задача сводится к вычислению некоторой функции ex(x, eps). Программа, написанная на языке Java в традиционном стиле, может выглядеть так, как показано в листинге 1.4.

Листинг 1.4. Вычисление числа e в традиционном стилеpublic static double ex(double x, double eps) { double ex = 0; // результат вычислений, исходная сумма = 0 double u = 1; // промежуточное значение члена ряда int n = 1; // номер очередного члена ряда Тейлора while (Math.abs(u) >= eps) { // вычисление очередного члена ряда, суммирование

Page 17: ÔÓÍÊÖÈÎÍÀËÜÍÎÅ ÏÐÎÃÐÀÌÌÈÐÎÂÀÍÈÅ · рования конструкции для параллельного выполнения фрагментов

17

ex += u; u = u * x / n; n++; } return ex; // выдача результата работы}

Для императивного стиля программирования характерно, что про-грамма содержит повторяющиеся вычисления, записанные в виде цикла. Инвариантом цикла в данной программе служит тот фа кт, что переменная ex содержит частичную сумму ряда с (n-1) членом, а переменная u содер-жит значение очередного (следующего) члена этого ряда.

Для написания той же программы в функцио нальном стиле следует сна-чала написать функцию, определяющую зависимость ex от x и eps, явно. Такая зависимость может быть выражена с помощью рекуррентного соот-ношения, которое определяет способ вычисления частичных сумм ряда. Если уже имеется вычисленная частичная сумма ряда Sn и очередной член ряда Un, то вычисления можно заканчивать, если значение Un меньше заданного eps. Если же это не так, то требуется добавить очередной член ряда к сумме, вычислить значение очередного члена ряда и вызвать ту же функцию рекурсивно.

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

Листинг 1.5. Вычисление числа e в функцио нальном стиле/** Основная функция вычисления e в степени x. * @param x показатель степени, * @param eps точность вычислений. * @return e в степени x с заданной точностью. */public static double ex (double x, double eps) { // вычисление происходит с помощью вызова // вспомогательной рекурсивной функции. return exRec(x, eps, 0, 1, 1);}

/** Вспомогательная рекурсивная функция, * осуществляющая вычисления. * @param x показатель степени, * @param eps точность вычислений, * @param sum накопленная частичная сумма ряда, * @param u очередной член ряда, * @param n номер следующего члена ряда. * @return сумма ряда с заданной точностью. */public static double exRec( double x, double eps, double sum, double u, int n) { return Math.abs(u) < eps ? sum : exRec(x, eps, sum + u, u * x / n, n + 1);}

Page 18: ÔÓÍÊÖÈÎÍÀËÜÍÎÅ ÏÐÎÃÐÀÌÌÈÐÎÂÀÍÈÅ · рования конструкции для параллельного выполнения фрагментов

18

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

Задача 1.2. Напишите программу для вычисления приближенного зна-чения корня уравнения cos x = x. Вычисления проводите методом бисекции (последовательного деления промежутка, содержащего корень функции, пополам). Предложите программы на языке Java, написанные в традици-онном императивном и функцио нальном стилях.

Решение. Задача нахождения корня функции методом бисекции может быть решена в более общем виде, чем для решения одного конкретного уравнения. Необходимым условием для применимости метода служит наличие интервала, где непрерывная функция имеет единственный корень. В нашем случае непрерывная на всей вещественной оси функция cos x -- x = 0 имеет единственный корень на промежутке [0, π/2] (это очевидно, например, из графика функции). В программе, написанной на языке Java в традиционном стиле, мы будем последовательно сужать интервал нахож-дения корня, прекращая работу в момен т, когда длина интервала станет меньше заданной точности. Значение точности вычислений будет един-ственным аргумен том нашей основной функции. Программа может выгля-деть так, как показано в листинге 1.6.

Листинг 1.6. Нахождение корня функции методом бисекции/** Функция приближенного вычисления корня уравнения * cos x = x на интервале (0, pi/2) с заданной точностью. * @param eps Точность вычислений — положительное число. * @return Приближенное значение корня уравнения. */static double rootCos(double eps) { return root(x -> Math.cos(x) — x, eps, 0, Math.PI / 2);} /** Функция приближенного вычисления корня функции f на * заданном интервале (a, b). * Предполагается, что на этом интервале имеется ровно * один корень, и на концах интервала функция принимает * значения разных знаков. * @param f Функция, корень которой мы ищем. * @param eps Точность вычислений — положительное число. * @param a Левый конец интервала, содержащего корень. * @param b Правый конец интервала, содержащего корень. * @return Приближенное значение корня */public static double root(Function<Double, Double> f, double eps, double a, double b) { double fa = f.apply(a); // значение функции на левом // конце промежутка. double m, fm; // вспомогательные переменные.

Page 19: ÔÓÍÊÖÈÎÍÀËÜÍÎÅ ÏÐÎÃÐÀÌÌÈÐÎÂÀÍÈÅ · рования конструкции для параллельного выполнения фрагментов

19

// цикл последовательного деления промежутка, // содержащего корень функции, пополам. while (b — a > eps) { m = (a + b) / 2; fm = f.apply(m); if (Math.signum(fa) == Math.signum(fm)) { // функция имеет значения одного знака // на концах интервала [a,m] a = m; fa = fm; } else { // функция имеет значения одного знака // на концах интервала [m,b] b = m; } } return a;}

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

Листинг 1.7. Поиск корня функции методом бисекции в функцио нальном стиле

/** Функция приближенного вычисления корня уравнения * cos x = x на интервале (0, pi/2) с заданной точностью. * @param eps Точность вычислений — положительное число. * @return Приближенное значение корня уравнения. */static double rootCos(double eps) { final Function<Double,Double> f = x -> Math.cos(x) — x; return root(f, eps, 0, Math.PI / 2, Math.signum(f.apply(0)));} /** Основная функция вычисления корня функции * на заданном промежутке. * @param f Функция, корень которой ищем. * @param eps Точность нахождения корня. * @param a Левый конец промежутка. * @param b Правый конец промежутка. * @param fa Знак функции на левом конце промежутка. * @return Приближенное значение корня. */static double root( Function<Double,Double> f, double eps, double a, double b, double fa) { final double m = (a + b) / 2; final double fm = Math.signum(f.apply(m)); return b — a < eps ? // требуемая точность достигнута.

Page 20: ÔÓÍÊÖÈÎÍÀËÜÍÎÅ ÏÐÎÃÐÀÌÌÈÐÎÂÀÍÈÅ · рования конструкции для параллельного выполнения фрагментов

20

a : fa == fm ? root(f, eps, m, b, fm) : root(f, eps, a, m, fa);}

В приведенном выше решении рекурсия заменена циклом, а вместо условных операторов if используются условные выражения, представлен-ные тернарным оператором (?:). Также можно обратить внимание на то, что в решении фа ктически не используются переменные. Константы f, m, fm помечены атрибутом final для того, чтобы подчеркнуть, что эти пере-менные фа ктически просто обозначают вычисленные значения, никаких присваиваний в написанных функциях им не делается. Строго говоря, сле-довало бы приписать атрибут final также и всем аргумен там функций — они тоже являются константами, а не переменными.

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

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

Итак, основная рекурсивная функция получит в качестве аргумен тов участок массива и число элемен тов в искомой сумме. Если участок доста-точно большой, то функция разделит его на два и запустит два рекурсивных процесса поиска по отдельности в каждой из половин. Затем она после-довательно исследует наборы элемен тов, расположенных вблизи точки деления, и из всех полученных результатов будет найден максимальный. Поскольку все функции станут «чистыми» и не будут содержать присваи-ваний вообще, то достаточно интеллектуальная система программирования сможет оптимизировать вычисления, организовав параллельное вычисле-ние на нескольких процессорах или ядрах одного процессора (листинг 1.8).

Листинг 1.8. Решение задачи о вычислении максимальной суммы элемен тов

/** Функция вычисления максимальной суммы рядом стоящих * элемен тов во фрагмен те массива с положительными элемен тами. * @param arr Исходный массив. * @param ind Начальный индекс фрагмен та массива.

Page 21: ÔÓÍÊÖÈÎÍÀËÜÍÎÅ ÏÐÎÃÐÀÌÌÈÐÎÂÀÍÈÅ · рования конструкции для параллельного выполнения фрагментов

21

* @param len Длина фрагмен та массива. * @param count Количество элемен тов в суммах. * @return Максимальная из сумм рядом стоящих count элемен тов * в заданном фрагмен те массива. */public static int maxSum( int[] arr, int ind, int len, int count) { return len < count ? 0 : len == count ? sum(arr, ind, count) : len < 2*count — 1 ? // Делить массив на части нет смысла. Math.max(sum(arr, ind, count), maxSum(arr, ind+1, len-1, count)) : // Массив делится пополам, дополнительно // исследуется пограничная область. Math.max(maxSum(arr, ind, len/2, count), Math.max(maxSum(arr, ind+len/2, len-len/2, count), maxSum(arr, ind+len/2 — count + 1, 2*count-2, count)));}

/** Функция суммирования элемен тов фрагмен та массива. * @param arr Исходный массив. * @param ind Начальный индекс фрагмен та массива. * @param len Длина фрагмен та массива. * @return Сумма элемен тов массива. */public static int sum(int[] arr, int ind, int len) { return len == 0 ? 0 : arr[ind] + sum(arr, ind+1, len-1);}

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

Заäа÷и äëя саìостоятеëьноãо решения

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

Page 22: ÔÓÍÊÖÈÎÍÀËÜÍÎÅ ÏÐÎÃÐÀÌÌÈÐÎÂÀÍÈÅ · рования конструкции для параллельного выполнения фрагментов

Задача 1.4. Напишите функцию для нахождения заданного значения в упорядоченном массиве целых чисел методом двоичного поиска.

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

Задача 1.6. Напишите программу для вычисления n-го члена числовой последовательности, заданной следующим рекуррентным соотношением: a0 = a1 = 1, an+2 = 3an - 2an–1 +1 при n > 1.

Задача 1.7. Напишите программу для вычисления приближенного зна-чения функции cos x при заданном значении x. Для вычислений исполь-зуйте разложение косинуса в ряд Тейлора в окрестности нуля. Примените периодичность тригонометрических функций для приведения аргумен та к окрестности нуля для повышения точности вычислений.

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

Задача 1.9. Напишите программу для нахождения длины максимальной возрастающей подпоследовательности рядом стоящих элемен тов в задан-ном числовом массиве.

Page 23: ÔÓÍÊÖÈÎÍÀËÜÍÎÅ ÏÐÎÃÐÀÌÌÈÐÎÂÀÍÈÅ · рования конструкции для параллельного выполнения фрагментов

23

Гëава 2. ЭЛЕМЕН ТЫ ЯЗЫКА HASKELL

В результате изучения материала главы 2 студент должен:знать• базовые понятия языка программирования Haskell: типы, значения, функции;• базовый набор стандартных типов и функций языка Haskell;• понятие сопоставления с образцом, конструкторов списков;уметь• писать простейшие функции обработки числовой и символьной информации,

распознавать концевую рекурсию и пользоваться ею;• пользоваться механизмом накапливающих аргумен тов для хранения проме-

жуточных данных вычислений;• подготавливать и загружать файлы исходного кода, анализировать информацию

об ошибках;владеть• навыками написания простых программ на языке Haskell;• технологиями обработки списковых структур;• техникой преобразования рекурсивных функций в функции с концевой рекур-

сией.

2.1. Систеìа ïроãраììирования Haskell Platform

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

Систему Haskell Platform можно установить с сайта https://www.haskell.org, перейдя по ссылке https://www.haskell.org/platform/. В данной книге в иллюстрациях используется версия 7.6.3 платформы для операционной системы Windows, но все примеры можно непосредственно исполнять, используя любую версию платформы, поддерживающую версию языка Haskell не меньше Haskell 2010, для операционных систем Windows, Linux или MacOS. После установки системы можно сразу же запустить интер-претатор выражений, записанных на языке Haskell, — GHCi (интерпрета-тор командной строки) или (в системе Windows) WinGHCi. GHC (Glasgow Haskell Compiler) — это самый популярный на данный момен т компиля-тор программ на языке Haskell. Функцио нально оба интерпретатора GHCi и WinGHCi, а также аналогичные интерпретаторы для других операцион-ных систем совершенно одинаковы. В рисунках, приводимых в книге, мы

Page 24: ÔÓÍÊÖÈÎÍÀËÜÍÎÅ ÏÐÎÃÐÀÌÌÈÐÎÂÀÍÈÅ · рования конструкции для параллельного выполнения фрагментов

24

будем использовать интерпретатор командной строки GHCi для Windows, окно которого может выглядеть примерно так, как показано на риc. 2.1.

Риc. 2.1. Окно интерпретатора GHCi

В дальнейшем в тексте книги все примеры работы интерпретатора будут приводиться непосредственно в тексте. Например, если в интерпретаторе набрать выражение 2*2, то он тут же выведет результат вычисления — 4. В тексте книги мы будем показывать это следующим образом:>> 2*24

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

Мы уже выполнили одну команду, набрав выражение 2*2 для вычисле-ния. Теперь проверим, каков тип получаемого при этом результата. Ком-пилятор GHC умеет вычислять тип поданного ему выражения с помощью команды :type, которую можно задавать в сокращенном виде — :t:>> :t 2*22*2 :: Num a => a

Но разве тип выражения 2*2 — это не целое? Возможно, вы ожидали запи си наподобие>> :t 2*22*2 :: Integer

Запись Num a => a означает, что тип этого выражения может быть любым, переменная типа a означает фа ктически любой тип, но на этот тип наложено условие: он должен принадлежать классу Num, т.е. классу, для значений которого разрешены арифметические операции. Более подробно об арифметических классах будет рассказано в следующем параграфе.

Посмотреть на конкретный тип вычисленного выражения можно, вклю-чив опцию компилятора с помощью команды ":set +t" (показывать тип вычисленного значения). После включения этой опции интерпрета-тор после каждого вычисления будет также показывать тип вычисленного значения. Например, вывод интерпретатора после вычисления выражения 3.14 + 2*2 теперь будет таким:>> 3.14 + 2*27.140000000000001

Page 25: ÔÓÍÊÖÈÎÍÀËÜÍÎÅ ÏÐÎÃÐÀÌÌÈÐÎÂÀÍÈÅ · рования конструкции для параллельного выполнения фрагментов

25

it :: Doubleт.е. вычисленное значение имеет тип Double, что, впрочем, совершенно не-удивительно.

Теперь подадим интерпретатору следующую команду: "let a = 10". Никакого результата интерпретатор не выведет, зато теперь, подав команду "a*a", мы увидим следующий результат:>> a*a100it :: Integer

Похоже, что с помощью команды "let a = 10" мы выполнили что-то вроде присваивания. Но только что мы выяснили, что присваи-ваний в функцио нальном программировании нет! На самом деле это, конечно, не присваивание, а отождествление, мы просто дали значению 10 имя a, отождествив имя a с константой 10. Мы можем впоследствии отождествить с именем a другое значение, но это все равно не означает, что a — переменная. В этом легко убедиться, если попробовать написать «присваивание» "let a = a + 1". В действительности произойдет отождествление имени a с выражением a + 1, вычислить которое невоз-можно, — произойдет вход в бесконечную рекурсию. Если вы после подоб-ного определения попробуете дать команду интерпретатору на вычисление значения выражения a>> aто никакого результата не получите, интерпретатор уйдет в бесконечный цикл.

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

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

Итак, определим в программе функцию triangle с аргумен тами a, b и c, которая сначала введет обозначение p для значения полупериметра треугольника, а затем вычислит его площадь, используя как вычисленное значение полупериметра, так и аргумен ты — длины сторон треугольника. Вот как будет выглядеть текст нашей короткой программы (листинг 2.1).

Листинг 2.1. Программа для вычисления площади треугольникаtriangle a b c = let p = (a + b + c) / 2 in sqrt(p * (p-a) * (p-b) * (p-c))

Запишем нашу программу в текстовый файл с именем triangle.hs (расширение имени файла .hs используется для текстов программ

Page 26: ÔÓÍÊÖÈÎÍÀËÜÍÎÅ ÏÐÎÃÐÀÌÌÈÐÎÂÀÍÈÅ · рования конструкции для параллельного выполнения фрагментов

26

на языке Haskell). Пусть, например, полное имя файла в системе Windows будет C:\Haskell\triangle.hs. Теперь запустим наш интерпретатор и подадим команду на загрузку и проверку программы:>> :load C:\Haskell\triangle.hs

Если имя файла записано правильно и не содержит пробелов и букв, отличных от букв латинского алфавита, то наша программа будет загру-жена, и интерпретатор проверит правильность синтаксиса. Если все пра-вильно, то вы увидите на экране результат вроде[1 of 1] Compiling Main (C:\Haskell\triangle.hs, interpreted)Ok, modules loaded: Main.(0.02 secs, 0 bytes)>>

Это означает, что имя Main было выбрано интерпретатором «по умол-чанию», поскольку мы не задали никакого имени нашему «модулю» — он загружен, скомпилирован и готов к выполнению.

Теперь мы можем попробовать вычислить площадь какого-либо треу-гольника, записав выражение>> triangle 3 4 5,

что приведет к запуску нашей функции с аргумен тами 3, 4 и 5 и выдаче ожидаемого результата 6.0.

Итак, мы научились с помощью интерпретатора GHCi выполнять сле-дующие задачи:

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

(в функцио нальном программировании обычно говорят не «вызов», а «применение» функции);

• выполнять отождествление имени со значением.Проверьте, что вы действительно умеете выполнять все эти действия.

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

2.2. Эëеìен тарные тиïы äанных

Первая версия языка Haskell появилась в 1990 г. К концу 1990-х гг. он был довольно существенно переработан и упорядочен, а в 1998 г. появи-лась первая стандартная версия этого языка — Haskell 98. Язык сохра-нился без существенных изменений и в следующих версиях Haskell 2010 и Haskell 2012, все изменения, в основном, касались стандартных библио-тек; были также сделаны некоторые «подчистки» для повышения ясности и строгости языка. Он опирается на богатую историю создания функцио-нальных языков программирования, начатую первым таким языком — Лиспом, описанным в 1960 г. Джоном Маккарти (John McCarthy).

Page 27: ÔÓÍÊÖÈÎÍÀËÜÍÎÅ ÏÐÎÃÐÀÌÌÈÐÎÂÀÍÈÅ · рования конструкции для параллельного выполнения фрагментов

27

Сильное влияние на язык оказали следующие работы в области функцио нального программирования: система комбинаторного програм-мирования FP Джона Бэкуса, появившаяся в конце 1970-х гг., язык ML («Meta-language», созданный в Эдинбургском университете примерно в то же время) и появившийся в 1985—1986 гг. под сильным влиянием ML язык Miranda. Названными языками и системами мир функцио нального про-граммирования далеко не исчерпывается. В частности, названные языки и системы носят, скорее, «академический» или учебный характер, однако имеются и коммерческие языки, в которых принципы функцио нального программирования занимают центральное место, — Euler, Scala, F#.

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

Haskell — строго типизированный язык. Это означает, что любая кон-струкция в языке, задающая некоторый объект, имеет определенный тип, который можно «вычислить» еще до начала выполнения программы, т.е. статически. Таким же свойством обладают большинство современных языков программирования, такие как Паскаль, Java, C++ и многие другие. Языки, не имеющие строгой типизации, также распространены довольно широко. Таковыми являются многие из «скриптовых» языков, например Javascript. Стоит отметить, что языком без строгой типизации является также первый язык функцио нального программирования Лисп.

Основу системы типов языка Haskell составляют элемен тарные встро-енные типы данных: целые, представленные двумя подтипами с идентифи-каторами Integer и Int; вещественные, также с двумя подтипами Float и Double; логические, имеющие идентификатор типа Bool; символьные (Char). Заметим сразу же, что все идентификаторы в Haskell чувствительны к регистру букв, так что integer, Integer и INTEGER — это три разных идентификатора. Регистр первой буквы идентификатора определяет также, к какому из двух «миров» относится идентификатор: если идентификатор начинается с заглавной буквы, то он обозначает встроенный или опреде-ленный программистом тип или класс. С заглавных букв начинаются также имена модулей и пакетов. Идентификаторы объектов — значений простых и сложных типов, в том числе функций — начинаются со строчной буквы или символа подчеркивания. Идентификаторы, как и в других языках про-граммирования, строятся из букв, подчеркиваний и цифр, однако в Haskell допустимо использовать для построения идентификаторов еще и символ «'» («апостроф»). Ни апостроф, ни цифра не могут быть первым символом идентификатора.

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

Page 28: ÔÓÍÊÖÈÎÍÀËÜÍÎÅ ÏÐÎÃÐÀÌÌÈÐÎÂÀÍÈÅ · рования конструкции для параллельного выполнения фрагментов

28

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

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

Тип Integer определяет потенциально бесконечный набор целых чисел произвольной длины. Аналоги таких «длинных целых» имеются во многих языках программирования, например, BigInteger в языке Java. В операциях над целыми типа Integer никогда не происходит «перепол-нения» (конечно, в пределах выделенной для работы программы памяти). Для повышения эффективности работы программ можно использовать и вполне традиционные целые ограниченной длины. Для таких «ограни-ченных» целых применяется идентификатор типа Int. Последователь-ность цифр, возможно, предваренная знаком «–», представляет в программе целое число (как говорят, целые числа имеют литеральные обозначения в виде последовательности цифр).

Вещественные числа типов Float и Double определяются вполне тра-диционно и имеют также традиционные литеральные обозначения, такие как 3.14, -2.71828 или 0.12е-10.

Рассмотрим еще несколько особенностей языка Haskell, касающиеся изображений чисел. Во-первых, запись числа со знаком «+» перед ним некорректна. Неотрицательные числа принято записывать без знака. В то же время не следует воспринимать запись «–12» как отрицание (унарный минус) числа 12. Такой операции, как унарный минус, в языке нет совсем (но есть функция negate с аналогичным свойством). Обычно символы «+» и «–» означают традиционные двуместные операции сложения и вычита-ния, но если символ минуса записан перед числом, то это отрицательное число. Хорошей практикой будет всегда заключать отрицательные числа в скобки, чтобы избежать двусмысленностей. Например, выражение 2 * -1 записано некорректно, а -1 * 2 и 2 * (-1) корректны, и их вычисление дает ожидаемый результат –2.

Символьный тип также имеет литеральные обозначения для своих зна-чений, и они тоже вполне традиционны. Так, обозначение 'а' представ-ляет символ а.

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

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

Page 29: ÔÓÍÊÖÈÎÍÀËÜÍÎÅ ÏÐÎÃÐÀÌÌÈÐÎÂÀÍÈÅ · рования конструкции для параллельного выполнения фрагментов

29

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

Такое уточнение типа не обязательно, потому что система программи-рования на языке Haskell обязана сама устанавливать (выводить) типы всех встречающихся объектов и выражений, но иногда это может все же оказаться существенным. Например, литерал 239 в предыдущем примере может обозначать как целое типа Int, так и целое типа Integer, и даже вещественное типов Float или Double в зависимости от контекста. Явное указание типа Integer устраняет неоднозначность.

Так какой же тип имеет константа 239 в действительности?Проверка типа с помощью интерпретатора GHCi системы Haskell

Platform даст нам следующий результат:>> :t 239239 :: Num a => a

Прочитать эту запись следует так: выражение 239 имеет неопределен-ный тип a при условии, что этот тип принадлежит классу Num. Классы в языке Haskell имеют примерно тот же смысл, что и во многих других языках программирования: они определяют набор операций (функций), которые можно производить над объектами разных типов, принадлежащих этому классу. Например, полный список разрешенных операций для класса Num можно получить, набрав команду :info, в результате выполнения которой мы получим примерно следующий результат:>> :info Numclass Num a where (+) :: a -> a -> a (*) :: a -> a -> a (-) :: a -> a -> a negate :: a -> a abs :: a -> a signum :: a -> a fromInteger :: Integer -> a

В данном описании указаны все функции и операции, разрешенные для типов из этого класса, так что теперь понятно, что если, скажем, выполнить операцию сложения (+) над двумя значениями, каждое из которых имеет тип, принадлежащий классу Num, то в результате мы получим значение того же самого типа, что и типы операндов, так что результат выполнения команды>> :t 2+22+2 :: Num a => aне должен быть для вас удивительным.

Как быть с чуть более сложными выражениями, например, 3.14 + + 2*2? Команда проверки типа для этого выражения приведет к следую-щему результату:>> :t 3.14 + 2*23.14 + 2*2 :: Fractional a => a

Page 30: ÔÓÍÊÖÈÎÍÀËÜÍÎÅ ÏÐÎÃÐÀÌÌÈÐÎÂÀÍÈÅ · рования конструкции для параллельного выполнения фрагментов

30

Еще один класс:>> :info Fractionalclass Num a => Fractional a where (/) :: a -> a -> a recip :: a -> a fromRational :: Rational -> aв котором определены также несколько функций. Класс Fractional яв-ляется подклассом класса Num. Более подробно о системе классов мы по-говорим немного позже, а пока вернемся к типам числовых значений.

В языке Haskell нет неявных преобразований типов, но есть функции, позволяющие сделать такое преобразование явно, например, функция fromInteger, входящая в состав класса Num. Изображения чисел могут быть отнесены к разным типам в зависимости от того, в каком контексте число используется. Таким образом, число 2 и выражение 2*2 могут быть отнесены и к типу Integer, и к типу Float или Double в зависимости от контекста. Разумеется, сами вычисления производятся над значениями конкретных типов. Например, можно попросить интерпретатор вычислить значение выражения 3.14 + 2*2:>> 3.14 + 2*27.140000000000001it :: Double

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

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

Значения произвольных типов можно объединять в кортежи. Аналогом этого понятия в языке Паскаль будет определение типа запи си. Объеди-няемые в кортеж значения просто перечисляются в скобках через запя-тую. Например, обозначение (2, 'a') представляет кортеж из двух значений — целого числа 2 и символа 'а'. Заметим, что типом этого кор-тежа будет Num t => (t, Char), так что можно написать следующий фрагмен т программы, в котором вводится новое обозначение для этого кортежа и явно указывается его тип:pair :: (Int, Char)pair = (2, 'a')

Разумеется, кортежи сами могут входить в состав других кортежей. Например, следующий фрагмен т программы определяет идентификатор для кортежа, в состав которого входит другой кортеж:myValue :: (Int, (Char, Char), Bool)myValue = (366, ('A', 'K'), True)

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

Page 31: ÔÓÍÊÖÈÎÍÀËÜÍÎÅ ÏÐÎÃÐÀÌÌÈÐÎÂÀÍÈÅ · рования конструкции для параллельного выполнения фрагментов

31

можно посмотреть, например, в справке или в докумен тах по языку. Мы будем постепенно вводить новые операции и стандартные функции по мере необходимости, а пока отметим, что в языке имеется обычный набор ариф-метических и логических операций, а также операций сравнения и функ-ций преобразования значений из одних типов в другие. Запись выражений также традиционна, так что не вызывает никакого удивления, что, скажем, выражение 2+3 записано правильно и при вычислении выдает значение 5, а выражение 3<(2+2) выдает значение True. Наиболее необычными явля-ются две особенности запи си выражений.

Во-первых, при вызове функций аргумен ты отделяются от идентифи-катора функции не привычными скобками, а пробелом. Если аргумен тов несколько, то сами аргумен ты тоже отделяются друг от друга пробелами. Например, функцию вычисления синуса, определенную для вещественных аргумен тов, можно вызвать с помощью конструкцииsin 0.5,а функцию определения максимального из двух любых упорядоченных значений можно вызвать (в предположении, что идентификатор x обозна-чает некоторое числовое значение), скажем, так:max 3 (x+1)

В последнем примере скобки нужны для того, чтобы определить поря-док вычислений. Если бы выражение было записано как max 3 x+1, то интерпретатор прочитал бы его как (max 3 x)+1, т.е. «операция» приме-нения функции к аргумен ту считается более приоритетной, чем операция сложения. Фа ктически эта операция, не имеющая знака, является самой приоритетной в языке, так что выражение(sin x) * (cos y) — (sin y) * (cos x)может быть записано без скобок: sin x * cos y — sin y * cos x. Та же самая операция применения функции к аргумен ту имеет и собственный знак операции $, но этот знак, напротив, имеет самый низкий приоритет, так что в эквивалентной по смыслу запи си(sin $ x) * (cos $ y) — (sin $ y) * (cos $ x)скобки опустить уже нельзя. Зато выражениеsin (x + 2*y)можно записать без скобок в видеsin $ x + 2*y

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

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

Page 32: ÔÓÍÊÖÈÎÍÀËÜÍÎÅ ÏÐÎÃÐÀÌÌÈÐÎÂÀÍÈÅ · рования конструкции для параллельного выполнения фрагментов

32

идентификатор функции в обратные апострофы, так что приведенное выше обращение к функции вычисления максимального из двух упорядоченных значений можно было бы записать в виде применения бинарной операции:3 `max` (x+1)

Любопытно, что даже операция объединения значений в кортеж может быть записана в виде префиксной функции, так что вместо (2, True, 'a') можно написать эквивалентное выражение(,,) 2 True 'a'

Существуют стандартные функции для образования кортежей: (,), (,,), (,,,) и т.д. Таким образом, можно образовать кортежи из двух, трех, четырех и более значений. Впрочем, количество таких функций обра-зования кортежей все же ограничено. Например, в используемой автором версии компилятора максимальный размер кортежа — 62 элемен та. Суще-ствуют также функции, позволяющие извлечь элемен ты из кортежа, но их число совсем мало — это две функции fst и snd, с помощью которых можно извлечь первый и второй элемен ты двухэлемен тного кортежа. Для того чтобы извлечь, скажем, второй элемен т трехэлемен тного кортежа, придется написать свою (впрочем, очень простую) функцию.

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

2.3. Оïреäеëение функций с ïоìощью уравнений

Конечно, для того чтобы написать хоть сколь-нибудь полезную про-грамму, необходимо помимо новых идентификаторов для объектов и напи-сания выражений уметь самому определять новые функции. Каждая функ-ция в Haskell — это тоже некоторое значение, для которого определен тип. В типе функции указывают типы ее аргумен тов и тип значения функции, при этом типы аргумен тов и значения функции отделяются друг от друга символами "->", так что, например, тип функции одного вещественного аргумен та с вещественным результатом (такой, как, например, функция вычисления синуса) можно записать в видеDouble -> Doubleа тип функции с двумя целыми аргумен тами и одним логическим результа-том (такой, как, например, операция сравнения двух целых значений по ве-личине) — в видеInt -> Int -> Bool

Саму функцию можно определить с помощью «уравнения», в кото-ром выясняется, как функция должна себя вести, если ей задать значе-ние аргумен та. Например, определим функцию удвоения, которая удва-ивает значение своего вещественного аргумен та и выдает получившееся значение. Для этого зададим идентификатор функции twice, зададим ее тип и напишем уравнение, в котором покажем, что вызов этой функции с заданным значением аргумен та эквивалентен выражению, в котором это значение умножается на 2:twice :: Double -> Doubletwice x = 2 * x

Page 33: ÔÓÍÊÖÈÎÍÀËÜÍÎÅ ÏÐÎÃÐÀÌÌÈÐÎÂÀÍÈÅ · рования конструкции для параллельного выполнения фрагментов

33

Запишем нашу простую программу в некоторый файл, скажем, ~/Haskell/test.hs, и загрузим этот файл в интерпретатор в виде про-граммного модуля. Теперь мы можем использовать загруженную и ском-пилированную функцию для удвоения вещественных чисел:>> :cd ~/Haskell>> :load test.hs>> twice 3.146.28it :: Double

В описании функции мы явно указали, что ее аргумен том должно быть значение типа Double, при этом результат также будет иметь тип Double. Если же передать функции в качестве аргумен та целое число, то получится результат>> twice 1224.0it :: Double

Становится понятно, что число 12 здесь рассматривается как веще-ственное, поскольку контекст выражения twice 12 требует, чтобы аргумен т функции twice был вещественным. Здесь есть некоторое отли-чие от модели, привычной нам при работе с другими языками програм-мирования. Например, в языке Java число 12 рассматривается как целое, а при вызове функции, аргумен том которой должно быть вещественное число, неявно добавляется операция преобразования этого числа в веще-ственное. Здесь же само число 12 может рассматриваться как целое или вещественное в зависимости от контекста.

Вернемся к определению нашей простой функции. В уравнении, опре-деляющем поведение функции twice, можно выделить левую часть, зада-ющую форму вызова функции, и правую часть, в которой записывается выражение, заменяющее вызов функции в тот момен т, когда будет задано значение ее аргумен та. Если функция twice определена, то вычисление выражения twice 5.5 можно представить следующим образом. Сначала происходит сопоставление фа ктического значения аргумен та 5.5 с фор-мальным аргумен том x. Затем вызов функции заменяется правой частью уравнения, в которой вместо формального аргумен та используется сопо-ставленное с ним значение фа ктического аргумен та. Таким образом, после сопоставления и замены вместо выражения twice 5.5 получаем выраже-ние 2 * 5.5, которое после вычисления (применения операции умноже-ния) дает значение 11. Процесс преобразования (вычисления) выражения можно записать следующим образом:twice 5.5 → 2 * 5.5 → 11

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

Page 34: ÔÓÍÊÖÈÎÍÀËÜÍÎÅ ÏÐÎÃÐÀÌÌÈÐÎÂÀÍÈÅ · рования конструкции для параллельного выполнения фрагментов

34

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

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

Определение функции может быть рекурсивным, т.е. в правой части уравнения может быть вызов определяемой функции. В этом случае в про-цессе преобразования выражения, содержащего вызов рекурсивной функ-ции, может получаться выражение, также содержащее вызов той же самой функции. Для того чтобы процесс вычисления мог закончиться, необхо-димо использовать условные выражения, которые приводят к выбору одной из двух альтернатив при вычислении сложных выражений. Услов-ное выражение имеет видif <условие> then <выражение-"то"> else <выражение-"иначе">

При вычислении условного выражения прежде всего вычисляется условие. Тип вычисленного выражения-условия должен быть логиче-ским (Bool). Если вычисленное значение оказывается истинным (True), то вместо всего условного выражения подставляется выражение-«то», а выражение-«иначе» отбрасывается. Если же, наоборот, условие оказа-лось ложным (False), то отбрасывается выражение-«то», а вместо всего условного выражения остается только часть, определенная выражением-«иначе».

Зададим определение простой рекурсивной функции, предназначенной для вычисления фа кториала заданного целого числа. Простое и естествен-ное определение этой функции может выглядеть следующим образом:factorial :: Integer -> Integerfactorial n = if n == 0 then 1 else n * factorial (n-1)

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

Листинг 2.2. Процесс преобразования выражения при вычислении фа кториала числа

factorial 3if 3 == 0 then 1 else 3 * factorial 2if False then 1 else 3 * factorial 23 * factorial 23 * if 2 == 0 then 1 else 2 * factorial 13 * 2 * factorial 1