- •Оглавление
- •От автора
- •Структура
- •Пояснения и обозначения
- •Демонстрация кунг-фу
- •Теория Основные понятия и типы данных
- •Кортежи
- •Функции, операторы
- •Полиморфные типы данных
- •Чтение сигнатур типов
- •Простейшие функции и операторы
- •Арифметические функции
- •Логические функции
- •Списочные функции
- •Кортежные функции
- •Создание своих функций
- •Способ 1. Определение функции как выражения от параметров:
- •Способ 2. Несколько определений одной функции:
- •Способ 3. Определение функции через синоним:
- •Способ 4. Лямбда функция (анонимная функция):
- •Способ 5. Частичное применение функции:
- •Образцы и сопоставление с образцом
- •Синтаксический хлеб и синтаксический сахар
- •Условия и ограничения
- •Локальные определения
- •Двумерный синтаксис
- •Арифметические последовательности
- •Замыкания списков
- •Функциональное мышление
- •Рекурсия как основное средство
- •Ручная редукция выражений
- •Думаем функционально, шаг раз
- •Думаем функционально, шаг два: аккумуляторы
- •Реализация простейших списочных и прочих функций
- •Думаем функционально, шаг три: хвостовая рекурсия
- •Еще раз о рекурсии
- •Полезные хитрости языка
- •Ленивые вычисления и строгие функции
- •Бесконечные списки
- •Функция show
- •Совсем немного о классах
- •Функция read
- •Функция error
- •Побочные эффекты и функция trace
- •Функции высших порядков
- •Мотивация
- •Функция map
- •Функция filter
- •Композиция функций
- •Функция foldr
- •Функция foldl
- •Свертки: разбор полетов
- •Выявление общей функциональности
- •Стандартные функции высших порядков
- •Еще немного про строгие функции
- •Создание своих типов данных
- •Простые перечислимые типы данных
- •Контейнеры
- •О сравнении, отображении и прочих стандартных операциях
- •Параметрические типы данных
- •Сложные типы данных
- •Тип данных Maybe
- •Рекурсивные типы данных: списки
- •Рекурсивные типы данных: деревья
- •Ввод-вывод
- •Простейший ввод-вывод
- •Объяснение кухни
- •Пример программы, производящей нетривиальное преобразование текстового файла
- •Пример решения задачи: Поиск в пространстве состояний
- •Через массивы и последовательность промежуточных состояний
- •Решение для тех, кто не хочет разбираться сам
- •Через списки, лог истории и уникальную очередь
- •Решение для тех, кто не хочет разбираться сам
- •Задачник
- •Пояснения и обозначения
- •Лабораторная работа 1 Простейшие функции
- •Простейшие логические функции
- •Простейшие списочные функции
- •Лабораторная работа 2 Символьные функции
- •Простейшие кортежные функции
- •Теоретико-множественные операции
- •Сортировка
- •Арифметические последовательности
- •Генераторы списков
- •Лабораторная работа 4 Бесконечные списки
- •Ввод-вывод
- •Нетривиальные функции
- •Лабораторная работа 5 Простые числа и факторизация
- •Деревья
- •Деревья вычислений
- •Дополнительные задания для самостоятельной работы Задания с Project Euler
- •Простейший инструментарий Установка WinHugs и начало работы
- •Работа с интерпретатором WinHugs в интерактивном режиме
- •Команды интерпретатору
- •Работа с модулями
- •Список рекомендуемой литературы и электронных ресурсов
Еще немного про строгие функции
Возможно, этот раздел покажется вам немножко сложноватым. Давайте договоримся так: как только вы теряете нить объяснений – сразу бросайте это безнадежное дело, и переходите к следующему разделу. А сюда вернетесь тогда, когда ваши функции вдруг оказываются ужасно неэффективными (по времени или памяти) тогда, когда к этому, вроде бы, никаких предпосылок нет. Договорились?
Помните, мы говорили, что все функции в Haskell – нестрогие и ленивые? Вот у функции ($) есть специальный аналог ($!):
($) :: (a -> b) -> a -> b
($!) :: (a -> b) -> a -> b
Функция ($), как мы помним – это просто применение функции. Так вот, функция ($!) – это тоже просто применение функции, но – строгое. То есть, если функция ($) f x просто скажет что-то типа "эй, функция f, вот тебе x, дай-ка мне твой результат?", то функция ($!) f x сначала вычислит x, а потом уже передаст его функции f.
Тут можно было бы нарисовать смешной диалог. Если спросить у обычной функции (+): "Чему будет равно (+) 5 6 ?", – ленивая функция ответит: "5+6". А вот если был бы строгий аналог функции (+), он бы ответил честно: 11.
Как бы эту разницу увидеть? А вот, например, так:
SomeModule> length (take 10 (repeat $ (1/0)))
10
SomeModule> length (take 10 (repeat $! (1/0)))
Program error: {primDivDouble 1.0 0.0}
Что происходит в первом случае? Функция repeat создает целый список из нетерминальных выражений 1/0, мы потом пытаемся выбрать 10 элементов, и считаем, сколько выбралось. Как и положено ленивым функциям, результат вычисляется нормально, потому что repeat и не думает вычислять 1/0 – оно ей надо?
Зато во втором случае, оператор строгого применения ($!) вычисляет 1/0 перед тем, как передать его функции repeat, и, разумеется, вычисление обрывается с ошибкой.
Однако, при этом:
head [2, (1/) $! 0] → 2.0
length $! [map sin [1..]] → 1
Оставлю вас самих разбираться, почему в этом случае вроде бы нетерминальные конструкции так и не вычисляются (в смысле – не приводят к ошибке в вычислении основного выражения).
Как правило, мы не будем сталкиваться с необходимостью делать строгие функции вместо нестрогих. Но вы можете столкнуться с тем, что при вычислении очень глубокой рекурсии (даже и хвостовой), накапливаемый параметр разбухает так, что не умещается в стеке и программа падает – или не падает, но просто очень неэффективно работает. Попробуйте, например, вычислить выражение foldl (+) 0 [1..100000]? Казалось бы, чего там – просто посчитать сумму первых ста тысяч натуральных чисел – а ваш простой интерпретатор может и не справиться. Почему, ведь функция foldl даже реализована с использованием хвостовой рекурсии!
foldl :: (a -> b -> a) -> a -> [b] -> a
foldl f z [] = z
foldl f z (x:xs) = foldl f (f z x) xs
Проблема в том, что наш ленивый язык накапливает в аккумуляторе не частичные суммы (1,3,6,10,...) а конструкцию вида (((0+1)+2)+3)+... , в надежде, что эту сумму не придется вычислять. Но мы-то знаем, что придется! Для этого есть строгая версия функции foldl, которая отличается от нее только апострофом в названии (апостроф сам по себе к строгости отношения не имеет – он считается просто обычной буквой):
foldl' :: (a -> b -> a) -> a -> [b] -> a
foldl' f a [] = a
foldl' f a (x:xs) = (foldl' f $! f a x) xs
Для сравнения, перепишем немного определение функции foldl, чтобы очевидна стала разница (вы же видите, что это то же самое определение, что и абзацем назад?):
foldl :: (a -> b -> a) -> a -> [b] -> a
foldl f z [] = z
foldl f z (x:xs) = (foldl f $ (f z x)) xs
На каждом шаге функция foldl' действительно вычисляет новое значение аккумулятора, а не накапливает там ленивую очередь из вызовов! Проверьте, выражение foldl' (+) 0 [1..100000] вычисляется легко (чтобы воспользоваться функций foldl', придется, возможно, подключить модуль Data.List).