- •Оглавление
- •От автора
- •Структура
- •Пояснения и обозначения
- •Демонстрация кунг-фу
- •Теория Основные понятия и типы данных
- •Кортежи
- •Функции, операторы
- •Полиморфные типы данных
- •Чтение сигнатур типов
- •Простейшие функции и операторы
- •Арифметические функции
- •Логические функции
- •Списочные функции
- •Кортежные функции
- •Создание своих функций
- •Способ 1. Определение функции как выражения от параметров:
- •Способ 2. Несколько определений одной функции:
- •Способ 3. Определение функции через синоним:
- •Способ 4. Лямбда функция (анонимная функция):
- •Способ 5. Частичное применение функции:
- •Образцы и сопоставление с образцом
- •Синтаксический хлеб и синтаксический сахар
- •Условия и ограничения
- •Локальные определения
- •Двумерный синтаксис
- •Арифметические последовательности
- •Замыкания списков
- •Функциональное мышление
- •Рекурсия как основное средство
- •Ручная редукция выражений
- •Думаем функционально, шаг раз
- •Думаем функционально, шаг два: аккумуляторы
- •Реализация простейших списочных и прочих функций
- •Думаем функционально, шаг три: хвостовая рекурсия
- •Еще раз о рекурсии
- •Полезные хитрости языка
- •Ленивые вычисления и строгие функции
- •Бесконечные списки
- •Функция show
- •Совсем немного о классах
- •Функция read
- •Функция error
- •Побочные эффекты и функция trace
- •Функции высших порядков
- •Мотивация
- •Функция map
- •Функция filter
- •Композиция функций
- •Функция foldr
- •Функция foldl
- •Свертки: разбор полетов
- •Выявление общей функциональности
- •Стандартные функции высших порядков
- •Еще немного про строгие функции
- •Создание своих типов данных
- •Простые перечислимые типы данных
- •Контейнеры
- •О сравнении, отображении и прочих стандартных операциях
- •Параметрические типы данных
- •Сложные типы данных
- •Тип данных Maybe
- •Рекурсивные типы данных: списки
- •Рекурсивные типы данных: деревья
- •Ввод-вывод
- •Простейший ввод-вывод
- •Объяснение кухни
- •Пример программы, производящей нетривиальное преобразование текстового файла
- •Пример решения задачи: Поиск в пространстве состояний
- •Через массивы и последовательность промежуточных состояний
- •Решение для тех, кто не хочет разбираться сам
- •Через списки, лог истории и уникальную очередь
- •Решение для тех, кто не хочет разбираться сам
- •Задачник
- •Пояснения и обозначения
- •Лабораторная работа 1 Простейшие функции
- •Простейшие логические функции
- •Простейшие списочные функции
- •Лабораторная работа 2 Символьные функции
- •Простейшие кортежные функции
- •Теоретико-множественные операции
- •Сортировка
- •Арифметические последовательности
- •Генераторы списков
- •Лабораторная работа 4 Бесконечные списки
- •Ввод-вывод
- •Нетривиальные функции
- •Лабораторная работа 5 Простые числа и факторизация
- •Деревья
- •Деревья вычислений
- •Дополнительные задания для самостоятельной работы Задания с Project Euler
- •Простейший инструментарий Установка WinHugs и начало работы
- •Работа с интерпретатором WinHugs в интерактивном режиме
- •Команды интерпретатору
- •Работа с модулями
- •Список рекомендуемой литературы и электронных ресурсов
Композиция функций
Давайте еще раз посмотрим на последнюю функцию, которая выбрасывала из списка строк пустые:
nonEmptyStrings = filter (\s -> not (null s))
Что если нам наоборот нужно было оставлять только пустые, а выбрасывать все остальные? Такая функция записалась бы еще проще:
emptyStrings = filter null
Почему с непустыми строками приходится использовать лямбду? Потому что нам нужна не функция null, а функция противоположная ей: функция, которая возвращает True тогда, когда функция null возвращает False, и наоборот. Функция not превращает False в True, а True в False, но функция not не может превратить функцию null в противоположную ей! Это приходится делать с помощью лямбды: (\s -> not (null s)). Мы конструируем функцию, которой если передать строку, то она передаст ее в функцию null, потом результат функции null передаст в функцию not, и уже результат той вернет.
Такое поведение встречается очень часто: когда результат работы одной функции полностью передается на вход другой функции. В математике это называется композицией функций.
Точнее даже не так: композицией двух функций f и g называется функция, заключающаяся в последовательном применении к чему-то функции f, а затем функции g:
(.) :: (b -> c) -> (a -> b) -> a -> c
Вот она, композиция в Haskell, – функция с крайне непонятным, на первый взгляд, типом. Если присмотреться, то видно, что функция (.) принимает одну функцию типа (b -> c), еще одну функцию типа (a -> b), и возвращает функцию типа (a -> c).
Впрочем, если присмотреться по-другому, то видно, что функция (.) принимает одну функцию типа (b -> c), еще одну функцию типа (a -> b), элемент типа a и возвращает элемент типа c. Вот два эквивалентных определения функции (.), отражающих оба этих взгляда на композицию:
(.) f g = \x -> f (g x)
(.) f g x = f (g x)
С помощью композиции можно легко составлять сложные функции из двух простых. Например, если h = (+1).(^2), то h 5 → 26. Ну и если нужно вычислить функцию null, результат ее подать на вход функции not, то функция, делающая сразу и то и другое с помощью композиции записывается так: not.null.
Обращаю ваше внимание, что функция (.) работает именно с функциями, а не с данными. Она берет две функции и "состыковывает" их вместе, получая третью, сложную функцию.
Если опять представить себе две функции как передвижные ящики на колесиках, у которых есть по одному входному и одному выходному отверстию, то композицию этих функций можно получить, поставив эти два ящика рядом так, чтобы выходное отверстие одного вплотную прилегало к входному отверстию другого.
Обратите внимание, что данных на этой картинке еще нет, потому что композиция – это про функции, а не про данные. Данные могут вообще никогда не появиться, а результат работы композиции – вот он уже, готов и помаргивает огоньками.
Если вдруг когда кто-нибудь бросит что-то во входное отверстие первой функции, она сразу создаст свой выход, и он автоматически попадет на вход во вторую функцию, и преобразуется ей в что-то третье. Произойдет это без всякого вмешательства с нашей стороны, и поэтому можно будет такие два состыкованные ящика рассматривать как единый ящик, единую функцию.
Итак,
emptyStrings = filter null
nonEmptyStrings = filter (not.null)
С помощью композиции можно укорачивать запись преобразований, состоящих из нескольких этапов. Например, что если нужно каждый элемент списка увеличить на единицу, потом возвести в квадрат, а потом опять увеличить на единицу? Вот варианты:
foo1 xs = map (+1) (map (^2) (map (+1) xs))
foo2 = map (+1) . map (^2) . map (+1)
foo3 = map ((+1).(^2).(+1))
Или, например, нужно посчитать количество четных чисел в списке, не делящихся на 4:
foo1 xs = length (filter (\x -> x `mod` 2 == 0 && \x -> x `mod` 4 /= 0) xs)
foo2 = length .
filter (\x -> x `mod` 4 /= 0) .
filter (\x -> x `mod` 2 == 0)
foo3 = length .
filter ((/=0).(`mod` 4)) .
filter ((==0).(`mod` 2))
Видите, как в каких-то вариантах функций нет ни одного упоминания о данных вообще? Ни одного формального параметра – только манипуляции функциями: создание нужных функций путем частичного применения и композиции, и передача этих функций в функции высших порядков, и опять композиция...