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