
- •Оглавление
- •От автора
- •Структура
- •Пояснения и обозначения
- •Демонстрация кунг-фу
- •Теория Основные понятия и типы данных
- •Кортежи
- •Функции, операторы
- •Полиморфные типы данных
- •Чтение сигнатур типов
- •Простейшие функции и операторы
- •Арифметические функции
- •Логические функции
- •Списочные функции
- •Кортежные функции
- •Создание своих функций
- •Способ 1. Определение функции как выражения от параметров:
- •Способ 2. Несколько определений одной функции:
- •Способ 3. Определение функции через синоним:
- •Способ 4. Лямбда функция (анонимная функция):
- •Способ 5. Частичное применение функции:
- •Образцы и сопоставление с образцом
- •Синтаксический хлеб и синтаксический сахар
- •Условия и ограничения
- •Локальные определения
- •Двумерный синтаксис
- •Арифметические последовательности
- •Замыкания списков
- •Функциональное мышление
- •Рекурсия как основное средство
- •Ручная редукция выражений
- •Думаем функционально, шаг раз
- •Думаем функционально, шаг два: аккумуляторы
- •Реализация простейших списочных и прочих функций
- •Думаем функционально, шаг три: хвостовая рекурсия
- •Еще раз о рекурсии
- •Полезные хитрости языка
- •Ленивые вычисления и строгие функции
- •Бесконечные списки
- •Функция show
- •Совсем немного о классах
- •Функция read
- •Функция error
- •Побочные эффекты и функция trace
- •Функции высших порядков
- •Мотивация
- •Функция map
- •Функция filter
- •Композиция функций
- •Функция foldr
- •Функция foldl
- •Свертки: разбор полетов
- •Выявление общей функциональности
- •Стандартные функции высших порядков
- •Еще немного про строгие функции
- •Создание своих типов данных
- •Простые перечислимые типы данных
- •Контейнеры
- •О сравнении, отображении и прочих стандартных операциях
- •Параметрические типы данных
- •Сложные типы данных
- •Тип данных Maybe
- •Рекурсивные типы данных: списки
- •Рекурсивные типы данных: деревья
- •Ввод-вывод
- •Простейший ввод-вывод
- •Объяснение кухни
- •Пример программы, производящей нетривиальное преобразование текстового файла
- •Пример решения задачи: Поиск в пространстве состояний
- •Через массивы и последовательность промежуточных состояний
- •Решение для тех, кто не хочет разбираться сам
- •Через списки, лог истории и уникальную очередь
- •Решение для тех, кто не хочет разбираться сам
- •Задачник
- •Пояснения и обозначения
- •Лабораторная работа 1 Простейшие функции
- •Простейшие логические функции
- •Простейшие списочные функции
- •Лабораторная работа 2 Символьные функции
- •Простейшие кортежные функции
- •Теоретико-множественные операции
- •Сортировка
- •Арифметические последовательности
- •Генераторы списков
- •Лабораторная работа 4 Бесконечные списки
- •Ввод-вывод
- •Нетривиальные функции
- •Лабораторная работа 5 Простые числа и факторизация
- •Деревья
- •Деревья вычислений
- •Дополнительные задания для самостоятельной работы Задания с Project Euler
- •Простейший инструментарий Установка WinHugs и начало работы
- •Работа с интерпретатором WinHugs в интерактивном режиме
- •Команды интерпретатору
- •Работа с модулями
- •Список рекомендуемой литературы и электронных ресурсов
Функциональное мышление
Теперь, вооружившись целым арсеналом способов создания функций, умением использовать образцы и знанием об удобных и приятных синтаксических возможностях языка Haskell, мы начнем, собственно, писать свои собственные функции, и в процессе изучать эти самые синтаксические возможности.
На самом деле, синтаксис языка – это всегда отражение идей, которые в этом языке скрыты. И барьер, который вам вот-вот предстоит преодолеть, связан не с синтаксисом, а с мышлением. Мышление требуется в функциональных языках – ну просто совсем другое. Будьте готовы начать мыслить по-новому!
Рекурсия как основное средство
Из чего состоят программы в императивных языках? Из присваиваний, проверки условий и переходов. Ну и еще из циклов, которые на самом деле есть все это вместе взятое. Вместо присваиваний и переходов в функциональных языках есть рекурсия. Давайте вспомним еще раз функцию нахождения длины списка:
length :: [a] -> Int
length [] = 0
length (x:xs) = 1 + length xs
Как мыслил создатель этой функции? Предположим, нам дали какой-то список. Список – это такая хитрая структура данных, у которой видна только голова и хвост, причем длина хвоста не видна. Чтобы посчитать длину, надо пройтись по каждому элементу… Нет, у нас нет такой возможности! Вот идея! Надо посчитать длину хвоста, и к ней прибавить единицу. Постойте, а как посчитать длину хвоста? Надо воспользоваться той же самой функцией!
В итоге, функция запускает себя саму, передавая все время хвост списка, и этот хвост будет уменьшаться и уменьшаться, и в итоге выродится в пустой список. Значит, надо отдельно обработать случай, когда требуется вычислить длину пустого списка.
Решение задачи над списком с помощью рекурсии всегда заключается в следующем вопросе самому себе. Предположим, нужно решить какую-то задачу для списка xs. Предположим, что кто-то нам дал ответ на нашу задачу, но для списка tail xs. Сможем ли мы тогда сразу легко дать ответ и для всего списка xs тоже?
Посмотрите, ведь именно такая логика скрыта в функции length! Как задача нахождения длины списка (x:xs) решается при условии, что уже известна длина списка xs? Да просто прибавлением единицы!
В целом, рекурсивный способ решения задачи сводится к тому, что задача разбивается на подзадачи, для которых запускаются другие (или те же самые) функции. Если подзадачи всегда оказываются меньше размером, чем исходная задача, то такой подход приведет к тому, что в итоге функция дойдет до некоторого числа простейших случаев, в каждом из которых легко сразу дать ответ.
Пусть мне нужно решить задачу K. Предположим, что я умею решать задачи L, M и N, которые меньше по размеру, чем задача K. Можно ли, зная решения задач L, M и N создать на них основе решение для задачи K? Если можно, то у нас сразу готов рекурсивный алгоритм решения нашей задачи.
Математики тут вспомнят доказательство по методу индукции, а программисты вспомнят методы динамического программирования. Можно еще вспомнить о связи динамического программирования и рекурсий, эффективные и неэффективные рекурсии и хранение результатов промежуточных задач.
Вот, оцените, как такой подход (разделяй и властвуй, в рекурсивной реинкарнации) позволяет элегантно написать функцию сортировки (первое кунг-фу из введения):
sort [] = []
sort (x:xs) =
sort [y | y <- xs, y <= x] ++
[x] ++
sort [y | y <- xs, y > x]
Что тут происходит? Если нужно отсортировать пустой список, то ничего делать не нужно: результатом является пустой список. Если нужно отсортировать список, состоящий из головы x и хвоста xs, то результатом является:
отсортированный список из всех таких элементов y из xs, которые не больше x, плюс…
список из самого элемента x, плюс…
отсортированный список из всех таких элементов y из xs, которые больше x.
Кстати, а что у нас с типом функции, сможет ли компилятор сам его восстановить? Где мы дали подсказку компилятору о типе элементов нашего списка xs? Да вон же, там где мы сравнивали элементы друг с другом! Раз к элементам применяется операция сравнения, значит тип этих элементов должен обязательно принадлежать классу Ord:
sort :: Ord a => [a] -> [a]