- •Оглавление
- •От автора
- •Структура
- •Пояснения и обозначения
- •Демонстрация кунг-фу
- •Теория Основные понятия и типы данных
- •Кортежи
- •Функции, операторы
- •Полиморфные типы данных
- •Чтение сигнатур типов
- •Простейшие функции и операторы
- •Арифметические функции
- •Логические функции
- •Списочные функции
- •Кортежные функции
- •Создание своих функций
- •Способ 1. Определение функции как выражения от параметров:
- •Способ 2. Несколько определений одной функции:
- •Способ 3. Определение функции через синоним:
- •Способ 4. Лямбда функция (анонимная функция):
- •Способ 5. Частичное применение функции:
- •Образцы и сопоставление с образцом
- •Синтаксический хлеб и синтаксический сахар
- •Условия и ограничения
- •Локальные определения
- •Двумерный синтаксис
- •Арифметические последовательности
- •Замыкания списков
- •Функциональное мышление
- •Рекурсия как основное средство
- •Ручная редукция выражений
- •Думаем функционально, шаг раз
- •Думаем функционально, шаг два: аккумуляторы
- •Реализация простейших списочных и прочих функций
- •Думаем функционально, шаг три: хвостовая рекурсия
- •Еще раз о рекурсии
- •Полезные хитрости языка
- •Ленивые вычисления и строгие функции
- •Бесконечные списки
- •Функция show
- •Совсем немного о классах
- •Функция read
- •Функция error
- •Побочные эффекты и функция trace
- •Функции высших порядков
- •Мотивация
- •Функция map
- •Функция filter
- •Композиция функций
- •Функция foldr
- •Функция foldl
- •Свертки: разбор полетов
- •Выявление общей функциональности
- •Стандартные функции высших порядков
- •Еще немного про строгие функции
- •Создание своих типов данных
- •Простые перечислимые типы данных
- •Контейнеры
- •О сравнении, отображении и прочих стандартных операциях
- •Параметрические типы данных
- •Сложные типы данных
- •Тип данных Maybe
- •Рекурсивные типы данных: списки
- •Рекурсивные типы данных: деревья
- •Ввод-вывод
- •Простейший ввод-вывод
- •Объяснение кухни
- •Пример программы, производящей нетривиальное преобразование текстового файла
- •Пример решения задачи: Поиск в пространстве состояний
- •Через массивы и последовательность промежуточных состояний
- •Решение для тех, кто не хочет разбираться сам
- •Через списки, лог истории и уникальную очередь
- •Решение для тех, кто не хочет разбираться сам
- •Задачник
- •Пояснения и обозначения
- •Лабораторная работа 1 Простейшие функции
- •Простейшие логические функции
- •Простейшие списочные функции
- •Лабораторная работа 2 Символьные функции
- •Простейшие кортежные функции
- •Теоретико-множественные операции
- •Сортировка
- •Арифметические последовательности
- •Генераторы списков
- •Лабораторная работа 4 Бесконечные списки
- •Ввод-вывод
- •Нетривиальные функции
- •Лабораторная работа 5 Простые числа и факторизация
- •Деревья
- •Деревья вычислений
- •Дополнительные задания для самостоятельной работы Задания с Project Euler
- •Простейший инструментарий Установка WinHugs и начало работы
- •Работа с интерпретатором WinHugs в интерактивном режиме
- •Команды интерпретатору
- •Работа с модулями
- •Список рекомендуемой литературы и электронных ресурсов
Свертки: разбор полетов
В чем идея функций foldl и foldr? Сами функции мы написали, а теперь давайте запросим у интерпретатора их тип:
foldl :: (a -> b -> a) -> a -> [b] -> a
foldr :: (b -> a -> a) -> a -> [b] -> a
Обе функции принимают какую-то бинарную операцию, принимают нулевой элемент, он же есть начальное значение аккумулятора типа a, и принимают список значений какого-то типа b. Теперь посмотрим на тип операции: эта операция принимает аккумулятор a, очередной элемент b, и возвратить она должна новое значение аккумулятора a. Отличие сигнатуры типов только в том, в каком порядке принимает операция свои аргументы: (a -> b -> a) для foldl и (b -> a -> a) для foldr.
В чем идея этих функций? Робот map шел вдоль-по списку-конвейеру, преобразовывал каждый элемент и складывал обратно. Робот filter шел по списку-конвейеру, проверял каждый элемент и какие-то выкидывал. А какая аналогия здесь?
Такая аналогия на самом деле есть. Робот foldl тоже идет по списку, как и робот map, но в отличие от последнего, робот foldl имеет аккумулятор, то есть память. Он идет вдоль конвейера, держа в руках нечто полусобранное – текущее состояние аккумулятора. Робот берет каждый элемент, и применяет какую-то операцию к этому элементу и своему полусобранному предмету, получая в результате новый полусобранный предмет, с которым в руках идет дальше. В итоге, к концу конвейера в его руках будет нечто, потенциально собранное из всех предметов на конвейере.
Признаюсь, foldl – моя любимая функция. Чудится мне в ней аллегория на все возможные вычисления вообще. Ведь аккумулятор у foldl совсем ничем не ограничен – он может быть вообще произвольным. В аккумуляторе у foldl может находиться целиком все состояние памяти компьютера, и тогда каждый шаг функции foldl – это выполнение какой-то команды, взятой с конвейера, которая как-то изменяет состояние аккумулятора, то есть ей памяти, и передает это состояние на следующий шаг вычислений. Впечатляет, не так ли?
Для иллюстрации того, что может делать функция foldl (а делать она может все, или почти все), посчитаем с помощью нее сразу количество больших и маленьких букв в заданной строке. Для этого аккумулятор, передаваемый с шага на шаг, должен содержать текущее найденное количество больших и маленьких букв. Кортеж для этого пойдет лучше всего:
cnt xs =
foldl (\ (sm,bg) c ->
if isLower c then (sm+1,bg)
else (sm,bg+1))
(0,0)
xs
Причем формальный параметр xs можно и опустить, а при большом желании и без лямбды обойтись:
cnt = foldl docnt (0,0) where
docnt (sm,bg) c | isLower c = (sm+1,bg)
| otherwise = (sm,bg+1)
В заключение о свертках: попробуйте сами разобраться, могут ли функции foldr и foldl работать с бесконечными списками? Очень полезное мероприятие. Думаю, не ошибусь, если предскажу, что в процессе поиска правильного ответа на этот вопрос, вы несколько раз поменяете свое мнение :).
Выявление общей функциональности
Давайте оглянемся на найденные функции map, filter, foldl и foldr и подумаем, что мы сделали.
Мы рассматривали обычные функции, и выявляли в них общее – ту самую схему, идею, по которой строилась обработка данных. В некоторых случаях приходилось преобразовать определение функции для того, чтобы общность идеи стала видна. Мы знали, в каком направлении преобразовывать код именно потому, что прежде увидели это направление разумом.
Идея каждой функции была достаточно общей, чтобы охватить достаточно большое количество конкретных функций, встречающихся в жизни. Достаточно общей – но не более того. Например, функция map у нас преобразовывала каждый элемент списка с помощью общей функции, которой было доступно только значение элемента, – но не его порядковый номер, или значения соседних элементов. Вполне можно было бы написать и более общую функцию, которая бы позволяла обрабатывать элемент, учитывая его порядковый номер:
map :: (Integer -> a -> b) -> [a] -> [b]
map f xs = mapN f 1 xs
mapN f _ [] = []
mapN f n (x:xs) = f n x : mapN f (n+1) xs
Здесь каждый элемент преобразовывается с помощью функции f, которая принимает не только очередной элемент x, но и его порядковый номер n.
В таком случае, некоторые функции можно было бы записать проще именно с такой версией функции map. Помните, была у нас задача – занулить те числа в списке, которые совпадают со своим порядковым номером? Элементарно, Ватсон!
zeroSome xs = mapN (\n x -> if x==n then 0 else x) xs
Но подождите, ведь того же самого мы добились и с помощью обычной функции map, приклеив дополнительно к каждому элементу его номер?
zeroSome xs = map f (zip [1..] xs) where
f (n,x) = if x==n then 0 else x
Кстати, зная теперь, что такое композиция, мы перепишем последнюю функцию еще короче:
zeroSome = map f . zip [1..] where
f (n,x) = if x==n then 0 else x
Что получается? Да, модифицированная версия функции map может все то же самое, что и классическая версия функции map. Но часто ли требуется такое преобразование? Лучше так: достаточно ли часто требуется такая функциональность, чтобы устать от повторного использования zip?