- •Оглавление
- •От автора
- •Структура
- •Пояснения и обозначения
- •Демонстрация кунг-фу
- •Теория Основные понятия и типы данных
- •Кортежи
- •Функции, операторы
- •Полиморфные типы данных
- •Чтение сигнатур типов
- •Простейшие функции и операторы
- •Арифметические функции
- •Логические функции
- •Списочные функции
- •Кортежные функции
- •Создание своих функций
- •Способ 1. Определение функции как выражения от параметров:
- •Способ 2. Несколько определений одной функции:
- •Способ 3. Определение функции через синоним:
- •Способ 4. Лямбда функция (анонимная функция):
- •Способ 5. Частичное применение функции:
- •Образцы и сопоставление с образцом
- •Синтаксический хлеб и синтаксический сахар
- •Условия и ограничения
- •Локальные определения
- •Двумерный синтаксис
- •Арифметические последовательности
- •Замыкания списков
- •Функциональное мышление
- •Рекурсия как основное средство
- •Ручная редукция выражений
- •Думаем функционально, шаг раз
- •Думаем функционально, шаг два: аккумуляторы
- •Реализация простейших списочных и прочих функций
- •Думаем функционально, шаг три: хвостовая рекурсия
- •Еще раз о рекурсии
- •Полезные хитрости языка
- •Ленивые вычисления и строгие функции
- •Бесконечные списки
- •Функция show
- •Совсем немного о классах
- •Функция read
- •Функция error
- •Побочные эффекты и функция trace
- •Функции высших порядков
- •Мотивация
- •Функция map
- •Функция filter
- •Композиция функций
- •Функция foldr
- •Функция foldl
- •Свертки: разбор полетов
- •Выявление общей функциональности
- •Стандартные функции высших порядков
- •Еще немного про строгие функции
- •Создание своих типов данных
- •Простые перечислимые типы данных
- •Контейнеры
- •О сравнении, отображении и прочих стандартных операциях
- •Параметрические типы данных
- •Сложные типы данных
- •Тип данных Maybe
- •Рекурсивные типы данных: списки
- •Рекурсивные типы данных: деревья
- •Ввод-вывод
- •Простейший ввод-вывод
- •Объяснение кухни
- •Пример программы, производящей нетривиальное преобразование текстового файла
- •Пример решения задачи: Поиск в пространстве состояний
- •Через массивы и последовательность промежуточных состояний
- •Решение для тех, кто не хочет разбираться сам
- •Через списки, лог истории и уникальную очередь
- •Решение для тех, кто не хочет разбираться сам
- •Задачник
- •Пояснения и обозначения
- •Лабораторная работа 1 Простейшие функции
- •Простейшие логические функции
- •Простейшие списочные функции
- •Лабораторная работа 2 Символьные функции
- •Простейшие кортежные функции
- •Теоретико-множественные операции
- •Сортировка
- •Арифметические последовательности
- •Генераторы списков
- •Лабораторная работа 4 Бесконечные списки
- •Ввод-вывод
- •Нетривиальные функции
- •Лабораторная работа 5 Простые числа и факторизация
- •Деревья
- •Деревья вычислений
- •Дополнительные задания для самостоятельной работы Задания с Project Euler
- •Простейший инструментарий Установка WinHugs и начало работы
- •Работа с интерпретатором WinHugs в интерактивном режиме
- •Команды интерпретатору
- •Работа с модулями
- •Список рекомендуемой литературы и электронных ресурсов
Полезные хитрости языка
Настало время рассказать о некоторых полезных хитростях языка Haskell, которые, опять-таки, во многом отражают идеи функционального программирования.
Ленивые вычисления и строгие функции
Haskell – крайне ленивый язык. Идея ленивости, на самом деле, очень проста: никакое выражение не будет вычисляться, пока его результат не понадобится. Более того, мы видели, что вычисление рекурсивных функций, если его производить руками, превращается в последовательное раскрытие одних выражений во вторые, вторых в третьи, и так далее. Часто бывает так, что уже частично раскрытого выражения достаточно для того, чтобы функция могла вернуть результат – в таком случае, выражение так и не будет вычислено до конца.
Классический пример – функция const:
const :: a -> b -> a
const x _ = x
Обратили внимание, что функция не использует свой второй параметр вообще? Работает она так:
const "hello" 1 → "hello"
const "hello" False → "hello"
const "hello" (1/0) → "hello"
Обратили внимание на последний вызов? В качестве второго параметра мы передаем ошибочное, нетерминальное выражение – а функция все равно возвращает правильный результат. Если бы мы работали с обычными неленивыми, строгими функциями, то передавая в функцию нетерминальное выражение, мы бы получали в ответ тоже нетерминальное выражение.
Немного занудства, позволите? Если обозначать ошибочное, нетерминальное (вычисление которого никогда не закончится) выражение как bot, то строгие функции – это те, которые всегда сами сразу виснут или выкидывают ошибку, если им такое значение передать. То есть для строгих функций, f(...,bot,...) → bot. Для нестрогих, соответственно, это может быть так, а может быть и не так.
Haskell – язык ленивый. Он не станет вычислять значение выражения (1/0) в надежде, что оно никогда не понадобится – и в данном случае его лень оказывается оправданной. А функции Haskell, соответственно – нестрогими. Конечно, если бы нетерминальное значение передавалось в качестве первого параметра – мы получили бы ошибку:
const (1/0) "hello" → Program error: {primDivDouble 1.0 0.0}
Однако и в этом случае ошибки можно избежать – достаточно просто не требовать выводить значение функции const на экран, и вообще, не требовать использовать его как-либо:
length [const (1/0) "hello"] → 1
В данном случае, функции length не требуется вычислять значение функции const. От функции length только требуют выяснить, сколько значений находится в переданном ей списке – а это значение одно: const (1/0) "hello", вне зависимости от того, вернет эта функция одно значение, список значений или что-либо еще. А значит, функция length не будет вычислять результат этого выражения.
Бесконечные списки
Ленивые вычисления, кроме своих прочих особенностей (например, вы не можете быть уверены, что нечто будет вычислено в тот или иной момент, или в том или ином порядке), обладают мощной возможностью: они позволяют создавать бесконечные структуры данных, например, бесконечные списки.
Помните арифметические последовательности? Выражение [1..] означало бесконечный список натуральных чисел, но что это за зверь, и откуда он взялся – было совершенно не понятно. А вот посмотрите на функцию repeat:
repeat :: a -> [a]
repeat x = x : repeat x
Вот что будет, если попробовать вычислить ее значение и вывести на экран:
repeat 1 →
1 : repeat 1 →
1 : (1 : repeat 1) →
1 : (1 : (1 : repeat 1)) → ...
В общем, вы поняли – функция никогда не завершится. И вы будете смотреть на появляющиеся на экране единички, которые никогда не кончатся. Но ведь не обязательно выводить результат этой функции на экран? Можно спросить у нее, например, первый элемент, или первые несколько элементов:
head (repeat 1) → 1
take 5 (repeat 1) → [1,1,1,1,1]
Вот, например, полезная функция: replicate, дублирующая заданное значение заданное количество раз. Как можно ее реализовать?
replicate :: Int -> a -> [a]
replicate 0 _ = []
replicate n x = x : replicate (n-1) x
А можно и воспользоваться функцией repeat:
replicate :: Int -> a -> [a]
replicate n x = take n (repeat x)
А как получить тот самый список натуральных чисел?
naturals = naturalsFrom 1
naturalsFrom n = n : naturalsFrom (n+1)
Большая часть списочных функций, которые мы с вами писали, будет работать не только с обычными, но и с бесконечными списками. Но некоторые, разумеется, не будут – такие как length и last – и вполне понятно, почему.
Еще точнее: какие-то функции, если им передать бесконечный список, вернут конечный результат (например, head). Другие, если им передать бесконечный список, вернут тоже бесконечный список, с которым вполне можно работать, если не вычислять его целиком (например, drop). А третьи, если им передать бесконечный список, вообще ничего не вернут и зависнут (например, last). Обдумайте разницу.