- •Оглавление
- •От автора
- •Структура
- •Пояснения и обозначения
- •Демонстрация кунг-фу
- •Теория Основные понятия и типы данных
- •Кортежи
- •Функции, операторы
- •Полиморфные типы данных
- •Чтение сигнатур типов
- •Простейшие функции и операторы
- •Арифметические функции
- •Логические функции
- •Списочные функции
- •Кортежные функции
- •Создание своих функций
- •Способ 1. Определение функции как выражения от параметров:
- •Способ 2. Несколько определений одной функции:
- •Способ 3. Определение функции через синоним:
- •Способ 4. Лямбда функция (анонимная функция):
- •Способ 5. Частичное применение функции:
- •Образцы и сопоставление с образцом
- •Синтаксический хлеб и синтаксический сахар
- •Условия и ограничения
- •Локальные определения
- •Двумерный синтаксис
- •Арифметические последовательности
- •Замыкания списков
- •Функциональное мышление
- •Рекурсия как основное средство
- •Ручная редукция выражений
- •Думаем функционально, шаг раз
- •Думаем функционально, шаг два: аккумуляторы
- •Реализация простейших списочных и прочих функций
- •Думаем функционально, шаг три: хвостовая рекурсия
- •Еще раз о рекурсии
- •Полезные хитрости языка
- •Ленивые вычисления и строгие функции
- •Бесконечные списки
- •Функция show
- •Совсем немного о классах
- •Функция read
- •Функция error
- •Побочные эффекты и функция trace
- •Функции высших порядков
- •Мотивация
- •Функция map
- •Функция filter
- •Композиция функций
- •Функция foldr
- •Функция foldl
- •Свертки: разбор полетов
- •Выявление общей функциональности
- •Стандартные функции высших порядков
- •Еще немного про строгие функции
- •Создание своих типов данных
- •Простые перечислимые типы данных
- •Контейнеры
- •О сравнении, отображении и прочих стандартных операциях
- •Параметрические типы данных
- •Сложные типы данных
- •Тип данных Maybe
- •Рекурсивные типы данных: списки
- •Рекурсивные типы данных: деревья
- •Ввод-вывод
- •Простейший ввод-вывод
- •Объяснение кухни
- •Пример программы, производящей нетривиальное преобразование текстового файла
- •Пример решения задачи: Поиск в пространстве состояний
- •Через массивы и последовательность промежуточных состояний
- •Решение для тех, кто не хочет разбираться сам
- •Через списки, лог истории и уникальную очередь
- •Решение для тех, кто не хочет разбираться сам
- •Задачник
- •Пояснения и обозначения
- •Лабораторная работа 1 Простейшие функции
- •Простейшие логические функции
- •Простейшие списочные функции
- •Лабораторная работа 2 Символьные функции
- •Простейшие кортежные функции
- •Теоретико-множественные операции
- •Сортировка
- •Арифметические последовательности
- •Генераторы списков
- •Лабораторная работа 4 Бесконечные списки
- •Ввод-вывод
- •Нетривиальные функции
- •Лабораторная работа 5 Простые числа и факторизация
- •Деревья
- •Деревья вычислений
- •Дополнительные задания для самостоятельной работы Задания с Project Euler
- •Простейший инструментарий Установка WinHugs и начало работы
- •Работа с интерпретатором WinHugs в интерактивном режиме
- •Команды интерпретатору
- •Работа с модулями
- •Список рекомендуемой литературы и электронных ресурсов
Еще раз о рекурсии
В заключение, в этой главе я хотел бы еще раз вернуться к тому, о чем мы говорили чуть раньше:
Решение задачи над списком с помощью рекурсии всегда заключается в следующем вопросе самому себе. Предположим, нужно решить какую-то задачу для списка xs. Предположим, что кто-то нам дал ответ на нашу задачу, но для списка tail xs. Сможем ли мы тогда сразу легко дать ответ и для всего списка xs тоже?
Это, на самом деле, очень важная идея, и я хотел бы еще раз проиллюстрировать ее на поучительном примере. Возьмем опять функцию length:
length :: [a] -> Int
length [] = 0
length (x:xs) = 1 + length xs
Ну кто, скажите, в здравом уме будет при
написании этой функции рассуждать в
ключе "а что если кто-то уже вычислил
длину хвоста списка"? Этот пример ни
разу не поучительный по той причине,
что нашему разуму с самого начала
понятно, как посчитать длину списка:
что там считать-то, тыкай пальцем в
каждый элемент и называй следующее
натуральное число! И такая рекурсивная
запись ничего разуму не дает – просто
еще один странный способ выдирания
гланд через желудок вычисления
того, что всем ясно, как вычислять.
Давайте рассмотрим нетривиальный пример! Пусть есть мальчишки во дворе, обозначаемые буквами, и нужно найти все возможные способы разделения их на две команды. Нам нужна функция такого типа:
divide :: [a] -> [([a],[a])]
Она берет список, например, наших символов, и возвращает – вглядитесь в эту мешанину круглых и квадратных скобок – список кортежей вида ([a],[a]), где на первом месте стоит список всех тех, кто попал в первую команду, а на втором месте – кто попал во вторую.
На всякий случай. Я в курсе, что можно перебрать все бинарные представления чисел от 0 до 2N. Вы ведь понимаете, что речь совсем не об этом, правда? Мы учимся искать рекурсивные решения. Когда вы этому научитесь, то сможете в каждом случае решать, нужно ли рекурсивное решение, или удобнее нерекурсивное.
Простой случай для функции: если есть только я, то я могу образовать собой единственным или одну команду, или другую:
divide [me] = [ ([me],[]) , ([],[me]) ]
А что дальше? Ну как, мозг, есть варианты? Вот здесь, где пасует наше интуитивное алгоритмическое мышление, и выходит на передний план рекурсивный способ во всей его красе. Что если есть "я" и "все остальные", как нам разделиться всеми возможными способами на две команды?
Так вот – предположим, что "все остальные" уже умеют делиться на команды, и уже поделились всеми возможными способами! А как же я? Меня-то забыли! Надо модифицировать все возможные способы с учетом того, что еще есть я!
divide (me:others) = join me (divide others) where
Вот оно, мы свели задачу к другой: пусть есть набор всевозможных разделений мальчишек на команды, – как этому набору добавить еще и меня? Это и будет делать локальная функция join:
join :: a -> [([a],[a])] -> [([a],[a])]
Она берет меня и всевозможные деления на команды, и должна создавать новые всевозможные деления, – но уже с учетом меня.
join me [] = []
join me ((side1,side2):divisions) =
[(me:side1,side2),(side1,me:side2)] ++
join me divisions
Итак, если есть список возможных разделений, то нужно его рекурсивно обработать. Берем каждое разделение без учета меня (side1,side2), и в результат складываем два возможных разделения уже с учетом меня. Одно, где я присоединяюсь к первой команде, а второе – где я иду во вторую команду: [(me:side1,side2),(side1,me:side2)].
Вот, собственно, и все. Если выкинуть определения типов, то задача решается в несколько строчек:
divide [me] = [ ([me] , []) , ([] , [me]) ]
divide (me:others) = join me (divide others) where
join me [] = []
join me ((side1,side2):divisions) =
[(me:side1,side2),(side1,me:side2)] ++
join me divisions
Проверим?
SomeModule> divide "abc"
[("abc",""), ("bc","a"), ("ac","b"), ("c","ab"),
("ab","c"), ("b","ac"), ("a","bc"), ("","abc")]
Чем этот пример поучителен? Тем, что наше интуитивное умение строить в голове алгоритмы здесь откровенно пасует, пытаясь сразу представить себе алгоритм построения решения. Все, что интуиция может – это охватить один шаг: если есть 10 вариантов разбиения на команды для какой-то группы людей, то в случае, если появляется еще один кто-то, у нас становится 20 вариантов разбиения, потому что в каждом варианте новый человек может пойти либо в одну команду, либо в другую.
Интуиция может охватить только один шаг алгоритма – но большего то и не надо! Все остальное сделает правильно запущенная рекурсия!