Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
MakeevGA-Haskell-a4-shortcode_2014_05_31.doc
Скачиваний:
28
Добавлен:
19.01.2023
Размер:
1.79 Mб
Скачать

Функциональное мышление

Теперь, вооружившись целым арсеналом способов создания функций, умением использовать образцы и знанием об удобных и приятных синтаксических возможностях языка Haskell, мы начнем, собственно, писать свои собственные функции, и в процессе изучать эти самые синтаксические возможности.

На самом деле, синтаксис языка – это всегда отражение идей, которые в этом языке скрыты. И барьер, который вам вот-вот предстоит преодолеть, связан не с синтаксисом, а с мышлением. Мышление требуется в функциональных языках – ну просто совсем другое. Будьте готовы начать мыслить по-новому!

Рекурсия как основное средство

Из чего состоят программы в императивных языках? Из присваиваний, проверки условий и переходов. Ну и еще из циклов, которые на самом деле есть все это вместе взятое. Вместо присваиваний и переходов в функциональных языках есть рекурсия. Давайте вспомним еще раз функцию нахождения длины списка:

length :: [a] -> Int

length [] = 0

length (x:xs) = 1 + length xs

Как мыслил создатель этой функции? Предположим, нам дали какой-то список. Список – это такая хитрая структура данных, у которой видна только голова и хвост, причем длина хвоста не видна. Чтобы посчитать длину, надо пройтись по каждому элементу… Нет, у нас нет такой возможности! Вот идея! Надо посчитать длину хвоста, и к ней прибавить единицу. Постойте, а как посчитать длину хвоста? Надо воспользоваться той же самой функцией!

В итоге, функция запускает себя саму, передавая все время хвост списка, и этот хвост будет уменьшаться и уменьшаться, и в итоге выродится в пустой список. Значит, надо отдельно обработать случай, когда требуется вычислить длину пустого списка.

Решение задачи над списком с помощью рекурсии всегда заключается в следующем вопросе самому себе. Предположим, нужно решить какую-то задачу для списка xs. Предположим, что кто-то нам дал ответ на нашу задачу, но для списка tail xs. Сможем ли мы тогда сразу легко дать ответ и для всего списка xs тоже?

Посмотрите, ведь именно такая логика скрыта в функции length! Как задача нахождения длины списка (x:xs) решается при условии, что уже известна длина списка xs? Да просто прибавлением единицы!

В целом, рекурсивный способ решения задачи сводится к тому, что задача разбивается на подзадачи, для которых запускаются другие (или те же самые) функции. Если подзадачи всегда оказываются меньше размером, чем исходная задача, то такой подход приведет к тому, что в итоге функция дойдет до некоторого числа простейших случаев, в каждом из которых легко сразу дать ответ.

Пусть мне нужно решить задачу K. Предположим, что я умею решать задачи L, M и N, которые меньше по размеру, чем задача K. Можно ли, зная решения задач L, M и N создать на них основе решение для задачи K? Если можно, то у нас сразу готов рекурсивный алгоритм решения нашей задачи.

Математики тут вспомнят доказательство по методу индукции, а программисты вспомнят методы динамического программирования. Можно еще вспомнить о связи динамического программирования и рекурсий, эффективные и неэффективные рекурсии и хранение результатов промежуточных задач.

Вот, оцените, как такой подход (разделяй и властвуй, в рекурсивной реинкарнации) позволяет элегантно написать функцию сортировки (первое кунг-фу из введения):

sort [] = []

sort (x:xs) =

sort [y | y <- xs, y <= x] ++

[x] ++

sort [y | y <- xs, y > x]

Что тут происходит? Если нужно отсортировать пустой список, то ничего делать не нужно: результатом является пустой список. Если нужно отсортировать список, состоящий из головы x и хвоста xs, то результатом является:

  • отсортированный список из всех таких элементов y из xs, которые не больше x, плюс…

  • список из самого элемента x, плюс…

  • отсортированный список из всех таких элементов y из xs, которые больше x.

Кстати, а что у нас с типом функции, сможет ли компилятор сам его восстановить? Где мы дали подсказку компилятору о типе элементов нашего списка xs? Да вон же, там где мы сравнивали элементы друг с другом! Раз к элементам применяется операция сравнения, значит тип этих элементов должен обязательно принадлежать классу Ord:

sort :: Ord a => [a] -> [a]