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

Думаем функционально, шаг два: аккумуляторы

Еще одна очень важная списочная функция: функция переворачивания списка, которая первый элемент ставит последним, второй – предпоследним, и так далее.

Следуя той логике, которую мы только что описали, эту функцию следует реализовать так:

slowReverse :: [a] -> [a]

slowReverse [] = []

slowReverse (x:xs) = slowReverse xs ++ [x]

Если вы так написали, то я вас поздравляю: вы успешно освоили первую ступень кунг-фу функционального программирования. Вы начали думать рекурсивно. Но по названию моей функции вы уже, наверное, догадались, в чем проблема. Да, функция slowReverse тоже имеет квадратичную сложность, потому что она вызывает саму себя N раз (где N – длина списка), и на каждом шаге вызывает функцию (++), которая тоже имеет линейную сложность по первому аргументу.

Что же делать? Может быть, поступить аналогично функции (++)?

quasiReverse [] = []

quasiReverse (x:xs) = x : quasiReverse xs

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

И вот в этом творческом тупике к нам на помощь и придет еще один важный прием функционального программирования. Помните, я уже говорил о том, что место локальных переменных в рекурсивном программировании занимают изменяемые локальные параметры? Так давайте введем дополнительный параметр, и будем в нем накапливать откусываемые головы от списка xs. Раз в этом параметре будет что-то накапливаться, то называться он будет – аккумулятор!

reverse :: [a] -> [a]

reverse xs = reverse2 xs []

reverse2 :: [a] -> [a] -> [a]

reverse2 [] acc = acc

reverse2 (x:xs) acc = reverse2 xs (x:acc)

Почему у нас две функции, reverse и reverse2? Потому что пользователю нужна функция reverse безо всяких лишних параметров, а функции reverse нужна вспомогательная функция с параметром. Вот как они работают:

reverse [1,2,3] →

reverse2 [1,2,3] [] →

reverse2 [2,3] (1 : []) →

reverse2 [3] (2 : (1 : [])) →

reverse2 [] (3 : (2 : (1 : []))) →

(3 : (2 : (1 : []))) →

[3,2,1]

Видите теперь, почему в аккумуляторе оказывается в итоге весь список в перевернутом виде? Потому что в начале вычисления функции аккумулятор пуст, а в процессе выполнения в него по одному заносятся элементы исходного списка – но заносятся всегда в начало!

Я предлагаю вам сейчас остановиться и еще раз посмотреть на тот прием, что мы применили. Вы должны научиться так думать, чтобы подобные приемы сами собой всплывали у вас в голове при решении задач со списками и не только. Сравните две вот эти строчки из функций quasiReverse и reverse2:

quasiReverse (x:xs) = x : quasiReverse xs

reverse2 (x:xs) acc = reverse2 xs (x:acc)

Давайте добавим во вторую функцию ничего не значащий и ничего не делающий аккумулятор:

quasiReverse (x:xs) acc = x : quasiReverse xs acc

reverse2 (x:xs) acc = reverse2 xs (x:acc)

И переименуем обе функции, чтобы действительная разница между двумя строчками была очевидна:

foo (x:xs) acc = x : foo xs ( acc)

foo (x:xs) acc = foo xs (x : acc)

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

Еще раз аккумуляторы мы вспомним чуть позже, когда будем говорить про хвостовую рекурсию.