
- •Оглавление
- •От автора
- •Структура
- •Пояснения и обозначения
- •Демонстрация кунг-фу
- •Теория Основные понятия и типы данных
- •Кортежи
- •Функции, операторы
- •Полиморфные типы данных
- •Чтение сигнатур типов
- •Простейшие функции и операторы
- •Арифметические функции
- •Логические функции
- •Списочные функции
- •Кортежные функции
- •Создание своих функций
- •Способ 1. Определение функции как выражения от параметров:
- •Способ 2. Несколько определений одной функции:
- •Способ 3. Определение функции через синоним:
- •Способ 4. Лямбда функция (анонимная функция):
- •Способ 5. Частичное применение функции:
- •Образцы и сопоставление с образцом
- •Синтаксический хлеб и синтаксический сахар
- •Условия и ограничения
- •Локальные определения
- •Двумерный синтаксис
- •Арифметические последовательности
- •Замыкания списков
- •Функциональное мышление
- •Рекурсия как основное средство
- •Ручная редукция выражений
- •Думаем функционально, шаг раз
- •Думаем функционально, шаг два: аккумуляторы
- •Реализация простейших списочных и прочих функций
- •Думаем функционально, шаг три: хвостовая рекурсия
- •Еще раз о рекурсии
- •Полезные хитрости языка
- •Ленивые вычисления и строгие функции
- •Бесконечные списки
- •Функция show
- •Совсем немного о классах
- •Функция read
- •Функция error
- •Побочные эффекты и функция trace
- •Функции высших порядков
- •Мотивация
- •Функция map
- •Функция filter
- •Композиция функций
- •Функция foldr
- •Функция foldl
- •Свертки: разбор полетов
- •Выявление общей функциональности
- •Стандартные функции высших порядков
- •Еще немного про строгие функции
- •Создание своих типов данных
- •Простые перечислимые типы данных
- •Контейнеры
- •О сравнении, отображении и прочих стандартных операциях
- •Параметрические типы данных
- •Сложные типы данных
- •Тип данных Maybe
- •Рекурсивные типы данных: списки
- •Рекурсивные типы данных: деревья
- •Ввод-вывод
- •Простейший ввод-вывод
- •Объяснение кухни
- •Пример программы, производящей нетривиальное преобразование текстового файла
- •Пример решения задачи: Поиск в пространстве состояний
- •Через массивы и последовательность промежуточных состояний
- •Решение для тех, кто не хочет разбираться сам
- •Через списки, лог истории и уникальную очередь
- •Решение для тех, кто не хочет разбираться сам
- •Задачник
- •Пояснения и обозначения
- •Лабораторная работа 1 Простейшие функции
- •Простейшие логические функции
- •Простейшие списочные функции
- •Лабораторная работа 2 Символьные функции
- •Простейшие кортежные функции
- •Теоретико-множественные операции
- •Сортировка
- •Арифметические последовательности
- •Генераторы списков
- •Лабораторная работа 4 Бесконечные списки
- •Ввод-вывод
- •Нетривиальные функции
- •Лабораторная работа 5 Простые числа и факторизация
- •Деревья
- •Деревья вычислений
- •Дополнительные задания для самостоятельной работы Задания с Project Euler
- •Простейший инструментарий Установка WinHugs и начало работы
- •Работа с интерпретатором WinHugs в интерактивном режиме
- •Команды интерпретатору
- •Работа с модулями
- •Список рекомендуемой литературы и электронных ресурсов
Функция read
Функция read в некотором смысле обратна функции show и используется тогда, когда нужно строку преобразовать в значение определенного типа. Вдумайтесь в ее тип:
read :: Read a => String -> a
Оценили? Функция получает строку, а возвращает значение произвольного (принадлежащего какому-то классу Read) типа a. Как такое может быть? Какой тип в итоге функция вернет?
Попробуем вызвать эту функцию, чтобы преобразовать строку в число:
Prelude> read "54"
ERROR - Unresolved overloading
*** Type : Read a => a
*** Expression : read "54"
Если вы прочитали прошлую главу про классы, то можете догадаться, что не нравится интерпретатору. В случае с функцией show он знал, какую конкретно перегруженную функцию незаметно от пользователя нужно вызвать: если вызывали show 5, он вызывал какую-нибудь функцию типа primitiveIntToString, если вызывали show True, он вызывал что-то вроде primitiveBoolToString. А если вызывали show sin, то он ругался, потому что не находил нужной примитивной функции, так как тип Double -> Double к классу Show не относится.
Короче говоря, в случае show было ясно, какой конкретно тип нужно преобразовать в строку. В случае функции read, интерпретатор не знает, значение какого типа нужно прочитать из строки. А как он это может узнать? А по тому, как мы будем с этим значением работать! Вот, например:
read "54" + 1 → 55
В данном случае контекст позволяет интерпретатору выявить, что преобразовать "54" нужно именно в число, потому что дальше с этим значением что-то складывается. Ну и, в конце концов, можно и напрямую попросить значение отнести к конкретному типу:
read "54" :: Integer → 54
read "54" :: Double → 54.0
Функция error
После функции read, тип функции error будет уже вполне понятен:
error :: String -> a
Эта функция принимает строку, а возвращает значение вообще произвольного типа. Раз так, то вставить ее можно в любую функцию – она везде по типу подойдет:
head [] = error “Program error: {head []}”
head (x:xs) = x
myDiv _ 0 = error "Program error: {primQrmInteger _ 0}"
myDiv x y = div x y
И в первом, и во втором случае функция error подходит по типу, а значение какого конкретно типа возвращается – уже не важно, потому что при вычислении значения этой функции, программа прерывается с ошибкой.
Побочные эффекты и функция trace
Говорят, что функция производит (имеет) побочный эффект, если ее результат может зависеть не только от ее собственных параметров, но и от ее окружения. Побочный эффект имеют функции, которые в процессе своих вычислений читают или модифицируют какие-то глобальные переменные, производят ввод-вывод (потому что это тоже есть чтение портов или модификация какой-нибудь видеопамяти). Если функцию с побочным эффектом вызвать дважды с одними и теми же входными значениями, она вполне может вернуть разный результат – потому что ее окружение, от которого она зависит, могло измениться.
Так вот, все функции, с которыми мы имели дело до сих пор в Haskell, побочных эффектов не имеют, и иметь не могут. Вы в принципе не сможете из своей функции что-то вывести на экран в процессе вычислений.
Это, конечно, ужасно неудобно, потому что как тогда отлаживать свои функции? Однако есть мнение, и автор с ним согласен, что потребность в трассировке в Haskell возникает гораздо реже – и именно потому, что большинство функций принципиально не могут иметь побочных эффектов.
Ну и еще, конечно, из-за строгой системы типов, через которую поначалу сложно продраться, но благодаря которой очень часто функция скомпилированная (с десятого раза) работает так, как нужно с первого раза. А какое соотношение у вас, в вашем сиплюсплюсе?
Однако возможности для трассировки все равно есть. Для этого нужно к вашему модулю подключить модуль Trace (или просто подгрузить этот модуль в командную строку командой :l Trace или :l Debug.Trace или import Debug.Trace, в зависимости от используемой среды), и тогда у вас появится функция trace:
trace :: String -> a -> a
Эта функция берет определенную строку, значение произвольного типа a, и возвращает то же самое значение, предварительно распечатав в консоли переданную строку:
Trace> trace "five" 5
five5
Trace> xx where xx = [(trace (show x) x)^2 | x <- [1..3]]
[11,24,39]
Что происходит в последней строчке? Генератор x <- [1..3] создает значение 1, вызывается функция trace (show 1) 1, которая распечатывает строку "1" и возвращает тоже 1, число 1 возводится в квадрат и выводится на экран. Потом генератор x <- [1..3] создает значение 2, вызывается функция trace (show 2) 2, которая распечатывает строку "2" и возвращает тоже 2, число 2 возводится в квадрат и выводится на экран, и так далее.
Однако при более глубоком рассмотрении, если вспомнить о ленивости языка, возникает очень много вопросов:
что является результатом вычисления выражения xx?
как выводится на экран результат вычисления выражения xx?
что выводится на экран в процессе вычисления выражения xx?
Это все очень разные вопросы, и вот, например, откуда это хорошо видно:
Trace> xx ++ xx where xx = [(trace (show x) x)^2 | x <- [1..3]]
[11,24,39,1,4,9]
Trace> sum xx where xx = [(trace (show x) x)^2 | x <- [1..3]]
12314
В последней строке "12314", выведенной на экран, "123" вывела функция trace при обработке списка [1,2,3], а "14" – это значение выражения sum [1,4,9]. Думаете, при таком нетривиальном поведении функции trace в связке с ленивыми функциями, вам будет легко найти глюк в своей функции? Ну-ну...